mirror of
https://github.com/Stone-Red-Code/DesktopMagic.git
synced 2026-11-01 23:04:16 +01:00
Allow sharting of themes and layouts via mod.io
This commit is contained in:
@@ -18,6 +18,7 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
|||||||
private string? selectedLayoutName;
|
private string? selectedLayoutName;
|
||||||
|
|
||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
|
private bool isSharing;
|
||||||
private string? pluginsSearchText;
|
private string? pluginsSearchText;
|
||||||
|
|
||||||
public string Title =>
|
public string Title =>
|
||||||
@@ -29,6 +30,19 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
|||||||
|
|
||||||
public string AppName => App.AppName;
|
public string AppName => App.AppName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a layout or theme is currently being uploaded to mod.io.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSharing
|
||||||
|
{
|
||||||
|
get => isSharing;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
isSharing = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public DesktopMagicSettings Settings
|
public DesktopMagicSettings Settings
|
||||||
{
|
{
|
||||||
get => settings;
|
get => settings;
|
||||||
|
|||||||
@@ -17,9 +17,38 @@ internal class PluginManagerDataContext : INotifyPropertyChanged
|
|||||||
private bool isLoading = true;
|
private bool isLoading = true;
|
||||||
private bool isSearching;
|
private bool isSearching;
|
||||||
private bool isAuthenticated = false;
|
private bool isAuthenticated = false;
|
||||||
|
private ModCategory category = ModCategory.Plugins;
|
||||||
|
|
||||||
public ObservableCollection<PluginEntryDataContext> AllPlugins { get; } = [];
|
public ObservableCollection<PluginEntryDataContext> AllPlugins { get; } = [];
|
||||||
public ObservableCollection<PluginEntryDataContext> InstalledPlugins { get; } = [];
|
public ObservableCollection<PluginEntryDataContext> InstalledPlugins { get; } = [];
|
||||||
|
|
||||||
|
// Layouts or themes, depending on the selected category.
|
||||||
|
public ObservableCollection<PluginEntryDataContext> AllShared { get; } = [];
|
||||||
|
public ObservableCollection<PluginEntryDataContext> InstalledShared { get; } = [];
|
||||||
|
|
||||||
|
public ModCategory Category
|
||||||
|
{
|
||||||
|
get => category;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
category = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(CategoryIndex));
|
||||||
|
OnPropertyChanged(nameof(ShownAll));
|
||||||
|
OnPropertyChanged(nameof(ShownInstalled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CategoryIndex
|
||||||
|
{
|
||||||
|
get => (int)category;
|
||||||
|
set => Category = (ModCategory)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<PluginEntryDataContext> ShownAll => category == ModCategory.Plugins ? AllPlugins : AllShared;
|
||||||
|
|
||||||
|
public ObservableCollection<PluginEntryDataContext> ShownInstalled => category == ModCategory.Plugins ? InstalledPlugins : InstalledShared;
|
||||||
|
|
||||||
public bool IsLoading
|
public bool IsLoading
|
||||||
{
|
{
|
||||||
get => isLoading;
|
get => isLoading;
|
||||||
@@ -97,4 +126,11 @@ internal class PluginManagerDataContext : INotifyPropertyChanged
|
|||||||
{
|
{
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal enum ModCategory
|
||||||
|
{
|
||||||
|
Plugins,
|
||||||
|
Layouts,
|
||||||
|
Themes
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,7 +49,6 @@
|
|||||||
<PackageReference Include="Interop.IWshRuntimeLibrary" Version="1.0.1" />
|
<PackageReference Include="Interop.IWshRuntimeLibrary" Version="1.0.1" />
|
||||||
<PackageReference Include="Ical.Net" Version="4.3.1" />
|
<PackageReference Include="Ical.Net" Version="4.3.1" />
|
||||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3650.58" />
|
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3650.58" />
|
||||||
<PackageReference Include="Modio" Version="1.0.0" />
|
|
||||||
<PackageReference Include="NAudio" Version="2.2.1" />
|
<PackageReference Include="NAudio" Version="2.2.1" />
|
||||||
<PackageReference Include="SkiaSharp" Version="4.148.0" />
|
<PackageReference Include="SkiaSharp" Version="4.148.0" />
|
||||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
<PackageReference Include="System.Management" Version="8.0.0" />
|
||||||
@@ -60,6 +59,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DesktopMagicPluginAPI\DesktopMagic.Api.csproj" />
|
<ProjectReference Include="..\DesktopMagicPluginAPI\DesktopMagic.Api.csproj" />
|
||||||
|
<ProjectReference Include="..\..\lib\modio.net\Modio\Modio.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,725 @@
|
|||||||
|
using DesktopMagic.Dialogs;
|
||||||
|
using DesktopMagic.Plugins;
|
||||||
|
using DesktopMagic.Settings;
|
||||||
|
|
||||||
|
using Modio;
|
||||||
|
using Modio.Filters;
|
||||||
|
using Modio.Models;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
using File = System.IO.File;
|
||||||
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
|
namespace DesktopMagic.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared mod.io access: authentication and sharing/syncing of layouts and themes.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ModIoService
|
||||||
|
{
|
||||||
|
public const uint GameId = 5665;
|
||||||
|
public const string ApiKey = "88e6ea774c3a502b06114e7fee0829ac";
|
||||||
|
|
||||||
|
private const string Source = "ModIo";
|
||||||
|
private const int MaxSummaryLength = 250;
|
||||||
|
|
||||||
|
private static readonly HttpClient httpClient = new();
|
||||||
|
private static (string Token, uint UserId)? currentUser;
|
||||||
|
|
||||||
|
public static bool IsAuthenticated => AppSettings.ModIoAccessToken is not null;
|
||||||
|
|
||||||
|
private static DesktopMagicSettings AppSettings => Manager.Instance.Settings;
|
||||||
|
|
||||||
|
public static Client CreateClient()
|
||||||
|
{
|
||||||
|
Client.Builder builder = CreateBuilder();
|
||||||
|
|
||||||
|
if (AppSettings.ModIoAccessToken is string token)
|
||||||
|
{
|
||||||
|
_ = builder.WithToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The shared api.mod.io host is deprecated, requests have to go to the game specific host.
|
||||||
|
/// </summary>
|
||||||
|
private static Client.Builder CreateBuilder()
|
||||||
|
{
|
||||||
|
return Client.GetBuilder(ApiKey).WithGameHost(GameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the mod is a plugin (as opposed to a shared layout or theme).
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsPluginMod(Mod mod)
|
||||||
|
{
|
||||||
|
return !mod.Tags.Any(tag => tag.Name is SharePackage.LayoutTag or SharePackage.ThemeTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the e-mail security code login. Returns false when cancelled.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<bool> LogInAsync(Window? owner, string title)
|
||||||
|
{
|
||||||
|
App.Logger.LogInfo("Starting mod.io authentication", source: Source);
|
||||||
|
Client client = CreateBuilder().Build();
|
||||||
|
|
||||||
|
InputDialog inputDialog = new(GetString("enterModIoEmail"), title)
|
||||||
|
{
|
||||||
|
Owner = owner,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (inputDialog.ShowDialog() != true)
|
||||||
|
{
|
||||||
|
App.Logger.LogInfo("Authentication cancelled by user", source: Source);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.Auth.RequestCode(ApiKey, inputDialog.ResponseText);
|
||||||
|
|
||||||
|
inputDialog = new(GetString("enterModIoAccessToken"), title)
|
||||||
|
{
|
||||||
|
Owner = owner,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (inputDialog.ShowDialog() != true)
|
||||||
|
{
|
||||||
|
App.Logger.LogInfo("Authentication cancelled by user", source: Source);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessToken accessToken = await client.Auth.SecurityCode(ApiKey, inputDialog.ResponseText);
|
||||||
|
if (accessToken.Value is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppSettings.ModIoAccessToken = accessToken.Value;
|
||||||
|
Manager.Instance.SaveSettings();
|
||||||
|
App.Logger.LogInfo("Successfully authenticated with mod.io", source: Source);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LogOut()
|
||||||
|
{
|
||||||
|
App.Logger.LogInfo("Logging out from mod.io", source: Source);
|
||||||
|
AppSettings.ModIoAccessToken = null;
|
||||||
|
currentUser = null;
|
||||||
|
Manager.Instance.SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compares the local content with the last published or downloaded version.
|
||||||
|
/// </summary>
|
||||||
|
public static void RefreshShareState(IModIoShareable item)
|
||||||
|
{
|
||||||
|
item.ShareState = item.ModIo switch
|
||||||
|
{
|
||||||
|
null => ModIoShareState.NotShared,
|
||||||
|
{ IsOwner: false } => ModIoShareState.NotOwner,
|
||||||
|
{ PublishedHash: string hash } when hash == SharePackage.Build(item).Hash => ModIoShareState.UpToDate,
|
||||||
|
_ => ModIoShareState.Modified,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Publishes the item as a new mod, uploads a new version of the linked mod, or opens the mod page when nothing changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="setBusy">Shows or hides the caller's waiting overlay while the upload runs.</param>
|
||||||
|
public static async Task ShareAsync(IModIoShareable item, Window? owner, Action<bool> setBusy)
|
||||||
|
{
|
||||||
|
if (item is Layout { Name: Manager.EmptyLayoutName })
|
||||||
|
{
|
||||||
|
await ShowMessageAsync(GetString("cannotShareEmptyLayout"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshShareState(item);
|
||||||
|
|
||||||
|
if (item.ShareState == ModIoShareState.UpToDate)
|
||||||
|
{
|
||||||
|
OpenProfile(item.ModIo?.ProfileUri);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsAuthenticated && !await LogInAsync(owner, App.AppName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SharePackage.Package package = SharePackage.Build(item);
|
||||||
|
bool isUpdate = item.ModIo is { IsOwner: true };
|
||||||
|
|
||||||
|
string? summary = null;
|
||||||
|
string? changelog = null;
|
||||||
|
|
||||||
|
if (isUpdate)
|
||||||
|
{
|
||||||
|
InputDialog changelogDialog = new(GetString("enterModIoChangelog")) { Owner = owner };
|
||||||
|
if (changelogDialog.ShowDialog() != true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
changelog = changelogDialog.ResponseText.Trim();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string warning = GetString(item is Layout ? "modIoShareLayoutWarning" : "modIoShareThemeWarning");
|
||||||
|
if (package.LocalPlugins.Count > 0)
|
||||||
|
{
|
||||||
|
warning += Environment.NewLine + Environment.NewLine + string.Format(GetString("modIoLocalPluginsWarning"), string.Join(", ", package.LocalPlugins));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await ConfirmAsync(warning, GetString("shareOnModIo")))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputDialog summaryDialog = new(GetString("enterModIoSummary")) { Owner = owner };
|
||||||
|
if (summaryDialog.ShowDialog() != true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary = summaryDialog.ResponseText.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(summary))
|
||||||
|
{
|
||||||
|
summary = item.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary.Length > MaxSummaryLength)
|
||||||
|
{
|
||||||
|
summary = summary[..MaxSummaryLength];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string workingDirectory = Path.Combine(Path.GetTempPath(), App.AppName, Guid.NewGuid().ToString());
|
||||||
|
setBusy(true);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = Directory.CreateDirectory(workingDirectory);
|
||||||
|
|
||||||
|
string contentDirectory = Path.Combine(workingDirectory, "content");
|
||||||
|
_ = Directory.CreateDirectory(contentDirectory);
|
||||||
|
await File.WriteAllTextAsync(Path.Combine(contentDirectory, SharePackage.ContentFileName), package.Json.ToJsonString());
|
||||||
|
|
||||||
|
string zipPath = Path.Combine(workingDirectory, "content.zip");
|
||||||
|
ZipFile.CreateFromDirectory(contentDirectory, zipPath);
|
||||||
|
|
||||||
|
string logoPath = Path.Combine(workingDirectory, "logo.png");
|
||||||
|
SharePreviewRenderer.Render(item, logoPath);
|
||||||
|
|
||||||
|
Client client = CreateClient();
|
||||||
|
ModIoLink link;
|
||||||
|
|
||||||
|
if (isUpdate)
|
||||||
|
{
|
||||||
|
link = item.ModIo!;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
App.Logger.LogInfo($"Creating mod.io mod for \"{item.Name}\"", source: Source);
|
||||||
|
NewMod newMod = new(item.Name, summary!, new FileInfo(logoPath))
|
||||||
|
{
|
||||||
|
Visible = Modio.Models.Visibility.Public,
|
||||||
|
};
|
||||||
|
newMod.Tags.Add(SharePackage.GetTag(item));
|
||||||
|
|
||||||
|
Mod mod = await client.Games[GameId].Mods.Add(newMod);
|
||||||
|
|
||||||
|
// A downloaded item shared as a new mod no longer follows the original, which would otherwise be downloaded again.
|
||||||
|
if (item.ModIo is { IsOwner: false } originalLink)
|
||||||
|
{
|
||||||
|
await UnsubscribeAsync(originalLink.ModId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the link right away, so a failed upload is retried on the same mod instead of creating another one.
|
||||||
|
link = new ModIoLink
|
||||||
|
{
|
||||||
|
ModId = mod.Id,
|
||||||
|
IsOwner = true,
|
||||||
|
ProfileUri = mod.ProfileUrl,
|
||||||
|
};
|
||||||
|
item.ModIo = link;
|
||||||
|
Manager.Instance.SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
App.Logger.LogInfo($"Uploading new version of \"{item.Name}\" (mod {link.ModId})", source: Source);
|
||||||
|
Modio.Models.File modfile = await client.Games[GameId].Mods[link.ModId].Files.Add(new NewFile(new FileInfo(zipPath))
|
||||||
|
{
|
||||||
|
Version = DateTime.UtcNow.ToString("yyyy.MM.dd.HHmm"),
|
||||||
|
Changelog = string.IsNullOrWhiteSpace(changelog) ? null : changelog,
|
||||||
|
Active = true,
|
||||||
|
}, new UploadOptions());
|
||||||
|
|
||||||
|
link.ModfileId = modfile.Id;
|
||||||
|
link.PublishedHash = package.Hash;
|
||||||
|
Manager.Instance.SaveSettings();
|
||||||
|
|
||||||
|
if (isUpdate)
|
||||||
|
{
|
||||||
|
await TryUpdateLogo(client, link.ModId, logoPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item is Layout)
|
||||||
|
{
|
||||||
|
await TryUpdateDependencies(client, link.ModId, package.RequiredPlugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshShareState(item);
|
||||||
|
App.Logger.LogInfo($"Published \"{item.Name}\" on mod.io", source: Source);
|
||||||
|
|
||||||
|
setBusy(false);
|
||||||
|
if (await ConfirmAsync(string.Format(GetString(isUpdate ? "modIoUpdateSuccess" : "modIoShareSuccess"), item.Name), GetString("open"), GetString("close")))
|
||||||
|
{
|
||||||
|
OpenProfile(link.ProfileUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (UnauthorizedException ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogError($"Sharing \"{item.Name}\" failed, not authorized: {ex.Message}", source: Source);
|
||||||
|
LogOut();
|
||||||
|
setBusy(false);
|
||||||
|
await ShowMessageAsync(GetString("modIoSessionExpired"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogError($"Sharing \"{item.Name}\" failed: {ex.Message}", source: Source);
|
||||||
|
setBusy(false);
|
||||||
|
await ShowMessageAsync(string.Format(GetString("modIoShareFailed"), ex.Message));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
setBusy(false);
|
||||||
|
RefreshShareState(item);
|
||||||
|
TryDeleteDirectory(workingDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribes from the mod an item is linked to, so deleting it locally does not bring it back on the next sync.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task UnsubscribeAsync(IModIoShareable item)
|
||||||
|
{
|
||||||
|
if (item.ModIo is not null)
|
||||||
|
{
|
||||||
|
await UnsubscribeAsync(item.ModIo.ModId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task UnsubscribeAsync(uint modId)
|
||||||
|
{
|
||||||
|
if (!IsAuthenticated)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await CreateClient().Games[GameId].Mods.Unsubscribe(modId);
|
||||||
|
App.Logger.LogInfo($"Unsubscribed from mod {modId}", source: Source);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogError($"Failed to unsubscribe from mod {modId}: {ex.Message}", source: Source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribes to the mods of downloaded layouts and themes, e.g. after logging in,
|
||||||
|
/// so the subscription sync keeps them instead of treating them as unsubscribed.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task SubscribeToLinkedItemsAsync()
|
||||||
|
{
|
||||||
|
if (!IsAuthenticated)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Client client = CreateClient();
|
||||||
|
|
||||||
|
foreach (ModIoLink link in GetSharedItems().Select(item => item.ModIo).OfType<ModIoLink>().Where(link => !link.IsOwner).ToList())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await client.Games[GameId].Mods.Subscribe(link.ModId);
|
||||||
|
App.Logger.LogInfo($"Subscribed to mod {link.ModId}", source: Source);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogError($"Failed to subscribe to mod {link.ModId}: {ex.Message}", source: Source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<IModIoShareable> GetSharedItems()
|
||||||
|
{
|
||||||
|
DesktopMagicSettings settings = AppSettings;
|
||||||
|
return settings.Layouts.Cast<IModIoShareable>().Concat(settings.Themes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IModIoShareable? FindLinkedItem(uint modId)
|
||||||
|
{
|
||||||
|
return GetSharedItems().FirstOrDefault(item => item.ModIo?.ModId == modId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downloads a layout or theme from mod.io and subscribes to it when logged in.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ensurePluginInstalled">Installs the mod.io plugin with the given ID if it is missing.</param>
|
||||||
|
/// <returns>Whether the item was installed.</returns>
|
||||||
|
public static async Task<bool> InstallAsync(Mod mod, Func<uint, Task> ensurePluginInstalled)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!await ApplyLatestVersionAsync(mod, FindLinkedItem(mod.Id), ensurePluginInstalled))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAuthenticated)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await CreateClient().Games[GameId].Mods.Subscribe(mod.Id);
|
||||||
|
App.Logger.LogInfo($"Subscribed to mod {mod.Id}", source: Source);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogError($"Failed to subscribe to mod {mod.Id}: {ex.Message}", source: Source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Manager.Instance.SaveSettings();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogError($"Failed to install \"{mod.Name}\" (mod {mod.Id}): {ex.Message}", source: Source);
|
||||||
|
await ShowMessageAsync(string.Format(GetString("modIoInstallFailed"), mod.Name, ex.Message));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates a linked layout or theme when mod.io has a newer version.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Whether the item changed.</returns>
|
||||||
|
public static async Task<bool> UpdateAsync(IModIoShareable item, Mod mod, Func<uint, Task> ensurePluginInstalled)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!await ApplyLatestVersionAsync(mod, item, ensurePluginInstalled))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Manager.Instance.SaveSettings();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogError($"Failed to update \"{item.Name}\" (mod {mod.Id}): {ex.Message}", source: Source);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a layout or theme locally after confirmation and unsubscribes from its mod.
|
||||||
|
/// The mod itself stays on mod.io.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Whether the item was removed.</returns>
|
||||||
|
public static async Task<bool> RemoveAsync(IModIoShareable item)
|
||||||
|
{
|
||||||
|
DesktopMagicSettings settings = AppSettings;
|
||||||
|
|
||||||
|
string? blocker = item switch
|
||||||
|
{
|
||||||
|
Layout { Name: Manager.EmptyLayoutName } => "cannotDeleteEmptyLayout",
|
||||||
|
Layout when settings.Layouts.Count <= 1 => "cannotDeleteLastLayout",
|
||||||
|
Theme when settings.Themes.Count <= 1 => "cannotDeleteLastTheme",
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (blocker is not null)
|
||||||
|
{
|
||||||
|
await ShowMessageAsync(GetString(blocker));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await ConfirmAsync(GetString(item is Layout ? "confirmDeleteLayout" : "confirmDeleteTheme"), "Yes", "No"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await UnsubscribeAsync(item);
|
||||||
|
|
||||||
|
switch (item)
|
||||||
|
{
|
||||||
|
case Layout layout:
|
||||||
|
Manager.Instance.RemoveLayout(layout);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Theme theme:
|
||||||
|
_ = settings.Themes.Remove(theme);
|
||||||
|
Manager.Instance.SaveSettings();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
App.Logger.LogInfo($"Removed \"{item.Name}\"", source: Source);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downloads subscribed layouts and themes that are new or have a newer version on mod.io.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscriptions">The user's subscribed mods for this game.</param>
|
||||||
|
/// <param name="ensurePluginInstalled">Installs the mod.io plugin with the given ID if it is missing.</param>
|
||||||
|
/// <returns>Whether any layout or theme changed.</returns>
|
||||||
|
public static async Task<bool> SyncSubscriptionsAsync(IReadOnlyList<Mod> subscriptions, Func<uint, Task> ensurePluginInstalled)
|
||||||
|
{
|
||||||
|
if (!IsAuthenticated)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
List<Mod> sharedMods = subscriptions.Where(mod => !IsPluginMod(mod)).ToList();
|
||||||
|
App.Logger.LogInfo($"Found {sharedMods.Count} subscribed layouts and themes on mod.io", source: Source);
|
||||||
|
|
||||||
|
foreach (Mod mod in sharedMods)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
changed |= await ApplyLatestVersionAsync(mod, FindLinkedItem(mod.Id), ensurePluginInstalled);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogError($"Failed to install \"{mod.Name}\" (mod {mod.Id}): {ex.Message}", source: Source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downloaded items whose subscription was removed on mod.io stay as local copies.
|
||||||
|
foreach (IModIoShareable item in GetSharedItems().Where(item => item.ModIo is { IsOwner: false } && !subscriptions.Any(mod => mod.Id == item.ModIo.ModId)).ToList())
|
||||||
|
{
|
||||||
|
App.Logger.LogInfo($"\"{item.Name}\" is no longer subscribed, keeping it as a local copy", source: Source);
|
||||||
|
item.ModIo = null;
|
||||||
|
RefreshShareState(item);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
Manager.Instance.SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Downloads the current version of the mod into a new item, or into <paramref name="local"/> when it is outdated.
|
||||||
|
/// Asks before overwriting local changes. Throws when the download or import fails.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Whether anything changed.</returns>
|
||||||
|
private static async Task<bool> ApplyLatestVersionAsync(Mod mod, IModIoShareable? local, Func<uint, Task> ensurePluginInstalled)
|
||||||
|
{
|
||||||
|
if (mod.Modfile?.Download?.BinaryUrl is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("The mod has no file to download.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (local?.ModIo?.ModfileId == mod.Modfile.Id)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (local?.ModIo is ModIoLink localLink && localLink.PublishedHash != SharePackage.Build(local).Hash)
|
||||||
|
{
|
||||||
|
bool overwrite = await ConfirmAsync(string.Format(GetString("modIoUpdateConflict"), local.Name), GetString("overwrite"), GetString("keepMine"));
|
||||||
|
|
||||||
|
if (!overwrite)
|
||||||
|
{
|
||||||
|
App.Logger.LogInfo($"Keeping local changes of \"{local.Name}\" instead of updating to mod {mod.Id} version {mod.Modfile.Id}", source: Source);
|
||||||
|
|
||||||
|
if (localLink.IsOwner)
|
||||||
|
{
|
||||||
|
// The owner can publish their version over the remote one.
|
||||||
|
localLink.ModfileId = mod.Modfile.Id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Detach the local copy so it is no longer replaced by updates.
|
||||||
|
await UnsubscribeAsync(local);
|
||||||
|
local.ModIo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
App.Logger.LogInfo($"Downloading \"{mod.Name}\" (mod {mod.Id}, version {mod.Modfile.Id})", source: Source);
|
||||||
|
JsonObject content = await DownloadContentAsync(mod.Modfile.Download.BinaryUrl);
|
||||||
|
|
||||||
|
foreach (uint pluginId in SharePackage.GetRequiredPlugins(content).Where(id => !Manager.Instance.Plugins.ContainsKey(id)))
|
||||||
|
{
|
||||||
|
await ensurePluginInstalled(pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint? userId = IsAuthenticated ? await GetCurrentUserIdAsync(CreateClient()) : null;
|
||||||
|
|
||||||
|
IModIoShareable item = SharePackage.Apply(content, local);
|
||||||
|
item.ModIo = new ModIoLink
|
||||||
|
{
|
||||||
|
ModId = mod.Id,
|
||||||
|
ModfileId = mod.Modfile.Id,
|
||||||
|
IsOwner = userId is not null && mod.SubmittedBy?.Id == userId,
|
||||||
|
ProfileUri = mod.ProfileUrl,
|
||||||
|
};
|
||||||
|
item.ModIo.PublishedHash = SharePackage.Build(item).Hash;
|
||||||
|
RefreshShareState(item);
|
||||||
|
|
||||||
|
App.Logger.LogInfo($"Installed \"{item.Name}\" from mod.io", source: Source);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<JsonObject> DownloadContentAsync(Uri url)
|
||||||
|
{
|
||||||
|
using MemoryStream zipStream = new();
|
||||||
|
using (Stream download = await httpClient.GetStreamAsync(url))
|
||||||
|
{
|
||||||
|
await download.CopyToAsync(zipStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
zipStream.Position = 0;
|
||||||
|
using ZipArchive archive = new(zipStream, ZipArchiveMode.Read);
|
||||||
|
ZipArchiveEntry entry = archive.GetEntry(SharePackage.ContentFileName) ?? throw new FormatException($"The download does not contain \"{SharePackage.ContentFileName}\".");
|
||||||
|
|
||||||
|
using StreamReader reader = new(entry.Open());
|
||||||
|
return SharePackage.Parse(await reader.ReadToEndAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<uint> GetCurrentUserIdAsync(Client client)
|
||||||
|
{
|
||||||
|
string token = AppSettings.ModIoAccessToken ?? string.Empty;
|
||||||
|
|
||||||
|
if (currentUser is not { } user || user.Token != token)
|
||||||
|
{
|
||||||
|
User me = await client.User.GetCurrentUser();
|
||||||
|
user = (token, me.Id);
|
||||||
|
currentUser = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.UserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task TryUpdateLogo(Client client, uint modId, string logoPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await client.Games[GameId].Mods[modId].AddMedia(new NewModMedia { Logo = new FileInfo(logoPath) });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogWarn($"Failed to update logo of mod {modId}: {ex.Message}", source: Source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task TryUpdateDependencies(Client client, uint modId, IReadOnlyList<uint> requiredPlugins)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DependenciesClient dependencies = client.Games[GameId].Mods[modId].Dependencies;
|
||||||
|
List<uint> current = (await dependencies.Get(false)).Select(dependency => dependency.ModId).ToList();
|
||||||
|
|
||||||
|
List<uint> removed = current.Except(requiredPlugins).ToList();
|
||||||
|
List<uint> added = requiredPlugins.Except(current).ToList();
|
||||||
|
|
||||||
|
if (removed.Count > 0)
|
||||||
|
{
|
||||||
|
await dependencies.Delete(removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (added.Count > 0)
|
||||||
|
{
|
||||||
|
await dependencies.Add(added);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogWarn($"Failed to update dependencies of mod {modId}: {ex.Message}", source: Source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OpenProfile(Uri? profileUri)
|
||||||
|
{
|
||||||
|
if (profileUri is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
UseShellExecute = true,
|
||||||
|
FileName = profileUri.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDeleteDirectory(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
Directory.Delete(path, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogWarn($"Failed to delete temporary directory {path}: {ex.Message}", source: Source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ShowMessageAsync(string content)
|
||||||
|
{
|
||||||
|
Wpf.Ui.Controls.MessageBox messageBox = new()
|
||||||
|
{
|
||||||
|
Title = App.AppName,
|
||||||
|
Content = content,
|
||||||
|
CloseButtonText = "Ok"
|
||||||
|
};
|
||||||
|
_ = await messageBox.ShowDialogAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> ConfirmAsync(string content, string confirmText, string? cancelText = null)
|
||||||
|
{
|
||||||
|
Wpf.Ui.Controls.MessageBox messageBox = new()
|
||||||
|
{
|
||||||
|
Title = App.AppName,
|
||||||
|
Content = content,
|
||||||
|
PrimaryButtonText = confirmText,
|
||||||
|
CloseButtonText = cancelText ?? GetString("cancel"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return await messageBox.ShowDialogAsync() == Wpf.Ui.Controls.MessageBoxResult.Primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetString(string key)
|
||||||
|
{
|
||||||
|
return App.LanguageDictionary[key] as string ?? key;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
using DesktopMagic.Plugins;
|
||||||
|
using DesktopMagic.Settings;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
namespace DesktopMagic.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts layouts and themes to and from the portable JSON content that is uploaded to mod.io.
|
||||||
|
/// </summary>
|
||||||
|
internal static class SharePackage
|
||||||
|
{
|
||||||
|
public const int FormatVersion = 1;
|
||||||
|
public const string LayoutTag = "Layout";
|
||||||
|
public const string ThemeTag = "Theme";
|
||||||
|
public const string ContentFileName = "content.json";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions jsonOptions = new()
|
||||||
|
{
|
||||||
|
Converters = { new ColorJsonConverter() }
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <param name="Json">The content written to <see cref="ContentFileName"/>.</param>
|
||||||
|
/// <param name="Hash">Hash of the shared content, used to detect local changes.</param>
|
||||||
|
/// <param name="RequiredPlugins">mod.io plugin IDs the layout uses.</param>
|
||||||
|
/// <param name="LocalPlugins">Names of locally developed plugins the layout uses, which others cannot install.</param>
|
||||||
|
public sealed record Package(JsonObject Json, string Hash, IReadOnlyList<uint> RequiredPlugins, IReadOnlyList<string> LocalPlugins);
|
||||||
|
|
||||||
|
public static string GetTag(IModIoShareable item) => item is Layout ? LayoutTag : ThemeTag;
|
||||||
|
|
||||||
|
public static Package Build(IModIoShareable item)
|
||||||
|
{
|
||||||
|
return item switch
|
||||||
|
{
|
||||||
|
Layout layout => BuildLayout(layout),
|
||||||
|
Theme theme => BuildTheme(theme),
|
||||||
|
_ => throw new ArgumentException($"Unsupported item type {item.GetType().Name}", nameof(item))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Package BuildTheme(Theme theme)
|
||||||
|
{
|
||||||
|
JsonObject root = new()
|
||||||
|
{
|
||||||
|
["format"] = FormatVersion,
|
||||||
|
["type"] = ThemeTag,
|
||||||
|
["themes"] = new JsonArray(SerializeTheme(theme)),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Package(root, ComputeHash(root), [], []);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Package BuildLayout(Layout layout)
|
||||||
|
{
|
||||||
|
DesktopMagicSettings settings = Manager.Instance.Settings;
|
||||||
|
IReadOnlyDictionary<uint, InternalPluginData> installedPlugins = Manager.Instance.Plugins;
|
||||||
|
|
||||||
|
Layout copy = new(layout.Name)
|
||||||
|
{
|
||||||
|
ScreenAspectRatio = layout.ScreenAspectRatio,
|
||||||
|
};
|
||||||
|
|
||||||
|
List<string> themeNames = [layout.Theme.Name];
|
||||||
|
List<uint> requiredPlugins = [];
|
||||||
|
List<string> localPlugins = [];
|
||||||
|
|
||||||
|
// Only enabled widgets are shared. Disabled entries are just placeholders for every installed plugin.
|
||||||
|
foreach ((uint pluginId, PluginSettings pluginSettings) in layout.Plugins.Where(kvp => kvp.Value.Enabled).OrderBy(kvp => kvp.Key))
|
||||||
|
{
|
||||||
|
copy.Plugins[pluginId] = new PluginSettings
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Position = pluginSettings.Position,
|
||||||
|
Size = pluginSettings.Size,
|
||||||
|
CurrentThemeName = pluginSettings.CurrentThemeName,
|
||||||
|
Settings = pluginSettings.Settings.Where(setting => setting.Shareable).ToList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pluginSettings.CurrentThemeName is not null && settings.Themes.Any(t => t.Name == pluginSettings.CurrentThemeName) && !themeNames.Contains(pluginSettings.CurrentThemeName))
|
||||||
|
{
|
||||||
|
themeNames.Add(pluginSettings.CurrentThemeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built-in plugins have no directory and are available everywhere.
|
||||||
|
if (installedPlugins.TryGetValue(pluginId, out InternalPluginData? pluginData) && !string.IsNullOrEmpty(pluginData.DirectoryPath))
|
||||||
|
{
|
||||||
|
if (pluginData.Metadata.IsLocalPlugin)
|
||||||
|
{
|
||||||
|
localPlugins.Add(pluginData.Metadata.Name);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
requiredPlugins.Add(pluginId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copy.CurrentThemeName = layout.Theme.Name;
|
||||||
|
|
||||||
|
JsonObject layoutNode = JsonSerializer.SerializeToNode(copy, jsonOptions)!.AsObject();
|
||||||
|
_ = layoutNode.Remove(nameof(Layout.ModIo));
|
||||||
|
|
||||||
|
JsonArray themesNode = [];
|
||||||
|
foreach (string themeName in themeNames)
|
||||||
|
{
|
||||||
|
Theme? theme = settings.Themes.FirstOrDefault(t => t.Name == themeName);
|
||||||
|
if (theme is not null)
|
||||||
|
{
|
||||||
|
themesNode.Add(SerializeTheme(theme));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject root = new()
|
||||||
|
{
|
||||||
|
["format"] = FormatVersion,
|
||||||
|
["type"] = LayoutTag,
|
||||||
|
["layout"] = layoutNode,
|
||||||
|
["themes"] = themesNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Required plugins are derived from the layout and depend on what is currently loaded, so they are not part of the hash.
|
||||||
|
string hash = ComputeHash(root);
|
||||||
|
root["requiredPlugins"] = new JsonArray(requiredPlugins.Select(id => (JsonNode)id).ToArray());
|
||||||
|
|
||||||
|
return new Package(root, hash, requiredPlugins, localPlugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonObject Parse(string json)
|
||||||
|
{
|
||||||
|
JsonObject root = JsonNode.Parse(json)?.AsObject() ?? throw new FormatException("The downloaded content is empty.");
|
||||||
|
|
||||||
|
int format = root["format"]?.GetValue<int>() ?? 0;
|
||||||
|
if (format is < 1 or > FormatVersion)
|
||||||
|
{
|
||||||
|
throw new FormatException($"Unsupported content format {format}. Please update DesktopMagic.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<uint> GetRequiredPlugins(JsonObject root)
|
||||||
|
{
|
||||||
|
return root["requiredPlugins"]?.AsArray().Select(node => node!.GetValue<uint>()).ToList() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the downloaded content to the settings, or updates <paramref name="existing"/> with it.
|
||||||
|
/// </summary>
|
||||||
|
public static IModIoShareable Apply(JsonObject root, IModIoShareable? existing)
|
||||||
|
{
|
||||||
|
DesktopMagicSettings settings = Manager.Instance.Settings;
|
||||||
|
string type = root["type"]?.GetValue<string>() ?? string.Empty;
|
||||||
|
|
||||||
|
List<Theme> themes = root["themes"]?.AsArray()
|
||||||
|
.Select(node => node.Deserialize<Theme>(jsonOptions))
|
||||||
|
.OfType<Theme>()
|
||||||
|
.ToList() ?? [];
|
||||||
|
|
||||||
|
if (type == ThemeTag)
|
||||||
|
{
|
||||||
|
Theme incoming = themes.FirstOrDefault() ?? throw new FormatException("The downloaded theme is empty.");
|
||||||
|
incoming.ModIo = null;
|
||||||
|
|
||||||
|
if (existing is Theme existingTheme)
|
||||||
|
{
|
||||||
|
CopyThemeValues(incoming, existingTheme);
|
||||||
|
return existingTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
incoming.Name = GetUniqueName(incoming.Name, settings.Themes.Select(t => t.Name));
|
||||||
|
settings.Themes.Add(incoming);
|
||||||
|
return incoming;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type != LayoutTag)
|
||||||
|
{
|
||||||
|
throw new FormatException($"Unknown content type \"{type}\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject layoutNode = root["layout"]?.AsObject() ?? throw new FormatException("The downloaded layout is empty.");
|
||||||
|
Layout incomingLayout = layoutNode.Deserialize<Layout>(jsonOptions) ?? throw new FormatException("The downloaded layout is invalid.");
|
||||||
|
string layoutName = existing?.Name ?? GetUniqueName(incomingLayout.Name, settings.Layouts.Select(l => l.Name));
|
||||||
|
|
||||||
|
Dictionary<string, string> themeNameMap = [];
|
||||||
|
foreach (Theme theme in themes)
|
||||||
|
{
|
||||||
|
themeNameMap[theme.Name] = AddEmbeddedTheme(theme, layoutName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the raw name, the getter falls back to another theme when the name does not exist yet.
|
||||||
|
string? layoutThemeName = layoutNode[nameof(Layout.CurrentThemeName)]?.GetValue<string>();
|
||||||
|
string? mappedLayoutThemeName = layoutThemeName is not null && themeNameMap.TryGetValue(layoutThemeName, out string? mapped) ? mapped : layoutThemeName;
|
||||||
|
|
||||||
|
Layout target = existing as Layout ?? new Layout(layoutName);
|
||||||
|
|
||||||
|
Dictionary<uint, PluginSettings> plugins = [];
|
||||||
|
foreach ((uint pluginId, PluginSettings incomingPlugin) in incomingLayout.Plugins)
|
||||||
|
{
|
||||||
|
if (incomingPlugin.CurrentThemeName is not null && themeNameMap.TryGetValue(incomingPlugin.CurrentThemeName, out string? pluginThemeName))
|
||||||
|
{
|
||||||
|
incomingPlugin.CurrentThemeName = pluginThemeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep values that are never shared (e.g. URLs or locations) and saved widget data from the local copy.
|
||||||
|
if (target.Plugins.TryGetValue(pluginId, out PluginSettings? localPlugin))
|
||||||
|
{
|
||||||
|
incomingPlugin.State = localPlugin.State;
|
||||||
|
incomingPlugin.Settings = [.. incomingPlugin.Settings, .. localPlugin.Settings.Where(local => !incomingPlugin.Settings.Exists(s => s.Id == local.Id))];
|
||||||
|
}
|
||||||
|
|
||||||
|
incomingPlugin.Owner = target;
|
||||||
|
plugins[pluginId] = incomingPlugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widgets the new version no longer uses are disabled, keeping their settings.
|
||||||
|
foreach ((uint pluginId, PluginSettings localPlugin) in target.Plugins.Where(kvp => !plugins.ContainsKey(kvp.Key)))
|
||||||
|
{
|
||||||
|
localPlugin.Enabled = false;
|
||||||
|
plugins[pluginId] = localPlugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.ScreenAspectRatio = incomingLayout.ScreenAspectRatio;
|
||||||
|
target.Plugins = plugins;
|
||||||
|
target.CurrentThemeName = mappedLayoutThemeName;
|
||||||
|
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
settings.Layouts.Add(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a theme that is embedded in a layout and returns the local name it is available under.
|
||||||
|
/// </summary>
|
||||||
|
private static string AddEmbeddedTheme(Theme theme, string layoutName)
|
||||||
|
{
|
||||||
|
DesktopMagicSettings settings = Manager.Instance.Settings;
|
||||||
|
theme.ModIo = null;
|
||||||
|
|
||||||
|
Theme? sameName = settings.Themes.FirstOrDefault(t => t.Name == theme.Name);
|
||||||
|
if (sameName is null)
|
||||||
|
{
|
||||||
|
settings.Themes.Add(theme);
|
||||||
|
return theme.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HasSameValues(sameName, theme))
|
||||||
|
{
|
||||||
|
return sameName.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A different theme with the same name exists (e.g. "Default"). Use a name tied to the layout,
|
||||||
|
// which later updates of the same layout overwrite instead of adding more copies.
|
||||||
|
string layoutThemeName = $"{theme.Name} ({layoutName})";
|
||||||
|
Theme? layoutTheme = settings.Themes.FirstOrDefault(t => t.Name == layoutThemeName);
|
||||||
|
|
||||||
|
if (layoutTheme is null)
|
||||||
|
{
|
||||||
|
theme.Name = layoutThemeName;
|
||||||
|
settings.Themes.Add(theme);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CopyThemeValues(theme, layoutTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
return layoutThemeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyThemeValues(Theme source, Theme target)
|
||||||
|
{
|
||||||
|
target.PrimaryColor = source.PrimaryColor;
|
||||||
|
target.SecondaryColor = source.SecondaryColor;
|
||||||
|
target.BackgroundColor = source.BackgroundColor;
|
||||||
|
target.Font = source.Font;
|
||||||
|
target.CornerRadius = source.CornerRadius;
|
||||||
|
target.Margin = source.Margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasSameValues(Theme a, Theme b)
|
||||||
|
{
|
||||||
|
JsonObject nodeA = SerializeTheme(a);
|
||||||
|
JsonObject nodeB = SerializeTheme(b);
|
||||||
|
_ = nodeA.Remove(nameof(Theme.Name));
|
||||||
|
_ = nodeB.Remove(nameof(Theme.Name));
|
||||||
|
|
||||||
|
return JsonNode.DeepEquals(nodeA, nodeB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject SerializeTheme(Theme theme)
|
||||||
|
{
|
||||||
|
JsonObject node = JsonSerializer.SerializeToNode(theme, jsonOptions)!.AsObject();
|
||||||
|
_ = node.Remove(nameof(Theme.ModIo));
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetUniqueName(string name, IEnumerable<string> existingNames)
|
||||||
|
{
|
||||||
|
HashSet<string> names = existingNames.ToHashSet();
|
||||||
|
if (!names.Contains(name))
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = 2;
|
||||||
|
while (names.Contains($"{name} ({index})"))
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{name} ({index})";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ComputeHash(JsonObject root)
|
||||||
|
{
|
||||||
|
byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(root.ToJsonString()));
|
||||||
|
return Convert.ToHexString(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
using DesktopMagic.Plugins;
|
||||||
|
using DesktopMagic.Settings;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
|
using Point = System.Windows.Point;
|
||||||
|
|
||||||
|
namespace DesktopMagic.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders the logo image used for layouts and themes on mod.io (mod.io requires one, at least 512x288).
|
||||||
|
/// </summary>
|
||||||
|
internal static class SharePreviewRenderer
|
||||||
|
{
|
||||||
|
private const int Width = 1280;
|
||||||
|
private const int Height = 720;
|
||||||
|
|
||||||
|
private static readonly Color canvasColor = Color.FromRgb(0x1C, 0x1C, 0x24);
|
||||||
|
private static readonly Color screenColor = Color.FromRgb(0x2E, 0x2E, 0x3A);
|
||||||
|
|
||||||
|
public static void Render(IModIoShareable item, string path)
|
||||||
|
{
|
||||||
|
DrawingVisual visual = new();
|
||||||
|
|
||||||
|
using (DrawingContext context = visual.RenderOpen())
|
||||||
|
{
|
||||||
|
context.DrawRectangle(new SolidColorBrush(canvasColor), null, new Rect(0, 0, Width, Height));
|
||||||
|
|
||||||
|
switch (item)
|
||||||
|
{
|
||||||
|
case Layout layout:
|
||||||
|
DrawLayout(context, layout);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Theme theme:
|
||||||
|
DrawTheme(context, theme);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderTargetBitmap bitmap = new(Width, Height, 96, 96, PixelFormats.Pbgra32);
|
||||||
|
bitmap.Render(visual);
|
||||||
|
|
||||||
|
PngBitmapEncoder encoder = new();
|
||||||
|
encoder.Frames.Add(BitmapFrame.Create(bitmap));
|
||||||
|
|
||||||
|
using FileStream stream = File.Create(path);
|
||||||
|
encoder.Save(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawLayout(DrawingContext context, Layout layout)
|
||||||
|
{
|
||||||
|
const double padding = 60;
|
||||||
|
double aspectRatio = layout.ScreenAspectRatio > 0 ? layout.ScreenAspectRatio : 16.0 / 9.0;
|
||||||
|
|
||||||
|
double availableWidth = Width - (padding * 2);
|
||||||
|
double availableHeight = Height - (padding * 2);
|
||||||
|
double screenWidth = Math.Min(availableWidth, availableHeight * aspectRatio);
|
||||||
|
double screenHeight = screenWidth / aspectRatio;
|
||||||
|
|
||||||
|
Rect screen = new((Width - screenWidth) / 2, (Height - screenHeight) / 2, screenWidth, screenHeight);
|
||||||
|
context.DrawRoundedRectangle(new SolidColorBrush(screenColor), null, screen, 12, 12);
|
||||||
|
|
||||||
|
foreach ((uint pluginId, PluginSettings pluginSettings) in layout.Plugins.Where(kvp => kvp.Value.Enabled))
|
||||||
|
{
|
||||||
|
Theme theme = pluginSettings.Theme;
|
||||||
|
|
||||||
|
Rect widget = new(
|
||||||
|
screen.X + (pluginSettings.Position.X * screen.Width),
|
||||||
|
screen.Y + (pluginSettings.Position.Y * screen.Height),
|
||||||
|
Math.Max(4, pluginSettings.Size.X * screen.Width),
|
||||||
|
Math.Max(4, pluginSettings.Size.Y * screen.Height));
|
||||||
|
widget.Intersect(screen);
|
||||||
|
|
||||||
|
if (widget.IsEmpty)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color background = MultiColorConverter.ConvertToMediaColor(theme.BackgroundColor);
|
||||||
|
if (background.A < 0x30)
|
||||||
|
{
|
||||||
|
background = Color.FromArgb(0x30, 0xFF, 0xFF, 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color primary = MultiColorConverter.ConvertToMediaColor(theme.PrimaryColor);
|
||||||
|
double radius = Math.Min(theme.CornerRadius * screen.Width / 1920, Math.Min(widget.Width, widget.Height) / 2);
|
||||||
|
|
||||||
|
context.DrawRoundedRectangle(new SolidColorBrush(background), new Pen(new SolidColorBrush(primary), 2), widget, radius, radius);
|
||||||
|
|
||||||
|
string name = Manager.Instance.Plugins.TryGetValue(pluginId, out InternalPluginData? pluginData) ? pluginData.Metadata.Name : pluginSettings.Metadata.Name;
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FormattedText text = CreateText(name, theme.Font, Math.Clamp(widget.Height / 4, 10, 28), primary);
|
||||||
|
text.MaxTextWidth = Math.Max(1, widget.Width - 12);
|
||||||
|
text.MaxTextHeight = Math.Max(1, widget.Height - 8);
|
||||||
|
text.Trimming = TextTrimming.CharacterEllipsis;
|
||||||
|
|
||||||
|
context.DrawText(text, new Point(widget.X + 6, widget.Y + ((widget.Height - Math.Min(text.Height, text.MaxTextHeight)) / 2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawTheme(DrawingContext context, Theme theme)
|
||||||
|
{
|
||||||
|
Color background = MultiColorConverter.ConvertToMediaColor(theme.BackgroundColor);
|
||||||
|
Color primary = MultiColorConverter.ConvertToMediaColor(theme.PrimaryColor);
|
||||||
|
Color secondary = MultiColorConverter.ConvertToMediaColor(theme.SecondaryColor);
|
||||||
|
|
||||||
|
Rect card = new(120, 100, Width - 240, Height - 200);
|
||||||
|
double radius = Math.Clamp(theme.CornerRadius * 2, 0, card.Height / 2);
|
||||||
|
|
||||||
|
context.DrawRoundedRectangle(new SolidColorBrush(screenColor), null, card, radius, radius);
|
||||||
|
context.DrawRoundedRectangle(new SolidColorBrush(background), null, card, radius, radius);
|
||||||
|
|
||||||
|
FormattedText title = CreateText(theme.Name, theme.Font, 96, primary);
|
||||||
|
title.MaxTextWidth = card.Width - 120;
|
||||||
|
title.MaxLineCount = 1;
|
||||||
|
title.Trimming = TextTrimming.CharacterEllipsis;
|
||||||
|
context.DrawText(title, new Point(card.X + 60, card.Y + 60));
|
||||||
|
|
||||||
|
FormattedText subtitle = CreateText(theme.Font, theme.Font, 48, secondary);
|
||||||
|
subtitle.MaxTextWidth = card.Width - 120;
|
||||||
|
subtitle.MaxLineCount = 1;
|
||||||
|
subtitle.Trimming = TextTrimming.CharacterEllipsis;
|
||||||
|
context.DrawText(subtitle, new Point(card.X + 60, card.Y + 60 + title.Height + 10));
|
||||||
|
|
||||||
|
Color[] swatches = [primary, secondary, background];
|
||||||
|
for (int i = 0; i < swatches.Length; i++)
|
||||||
|
{
|
||||||
|
Point center = new(card.X + 110 + (i * 130), card.Bottom - 110);
|
||||||
|
context.DrawEllipse(new SolidColorBrush(swatches[i]), new Pen(new SolidColorBrush(Color.FromArgb(0x60, 0x80, 0x80, 0x80)), 3), center, 50, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FormattedText CreateText(string text, string font, double size, Color color)
|
||||||
|
{
|
||||||
|
return new FormattedText(text, CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, new Typeface(font), size, new SolidColorBrush(color), 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -461,6 +461,27 @@ public sealed class Manager
|
|||||||
SaveSettings();
|
SaveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the layout, drops the screen bindings pointing to it and reloads the screens.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveLayout(Layout layout)
|
||||||
|
{
|
||||||
|
_ = Settings.Layouts.Remove(layout);
|
||||||
|
|
||||||
|
List<string> boundScreens = Settings.ScreenLayouts
|
||||||
|
.Where(kvp => kvp.Value == layout.Name)
|
||||||
|
.Select(kvp => kvp.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (string screenId in boundScreens)
|
||||||
|
{
|
||||||
|
_ = Settings.ScreenLayouts.Remove(screenId);
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveSettings();
|
||||||
|
LoadLayout();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all plugin windows currently shown on the given screen.
|
/// Gets all plugin windows currently shown on the given screen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||||
|
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:local="clr-namespace:DesktopMagic.Pages"
|
xmlns:local="clr-namespace:DesktopMagic.Pages"
|
||||||
@@ -12,97 +13,119 @@
|
|||||||
ScrollViewer.CanContentScroll="False"
|
ScrollViewer.CanContentScroll="False"
|
||||||
Title="MainPage">
|
Title="MainPage">
|
||||||
|
|
||||||
<Grid Margin="{StaticResource DefaultMargin}">
|
<busyIndicator:BusyMask IsBusy="{Binding IsSharing}" IndicatorType="ThreeDots" BusyContent="{DynamicResource uploadingToModIo}" BusyContentMargin="0,20,0,0" Foreground="{DynamicResource TextFillColorPrimaryBrush}" Background="{DynamicResource ControlStrokeColorDefaultBrush}">
|
||||||
<Grid.RowDefinitions>
|
<Grid Margin="{StaticResource DefaultMargin}">
|
||||||
<RowDefinition Height="auto" />
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="auto" />
|
||||||
<RowDefinition Height="auto" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
<RowDefinition Height="auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<ui:Card Padding="5" Margin="0 0 0 5">
|
<ui:Card Padding="5" Margin="0 0 0 5">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition />
|
|
||||||
<ColumnDefinition Width="10" />
|
|
||||||
<ColumnDefinition />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<ui:ToggleSwitch x:Name="editCheckBox" Height="30" VerticalAlignment="Center" Margin="5 0 0 0" Content="{DynamicResource editLayout}" Click="EditCheckBox_Click" />
|
|
||||||
<ui:TextBox Grid.Column="2" Text="{Binding PluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="AllPluginsSearchTextBox_TextChanged" PlaceholderText="{DynamicResource searchInstalledPlugins}" Icon="{ui:SymbolIcon Search24}" IconPlacement="Right" />
|
|
||||||
</Grid>
|
|
||||||
</ui:Card>
|
|
||||||
|
|
||||||
<ScrollViewer Background="#FFBBBBBB" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel" Grid.Row="1" VerticalAlignment="Stretch" VerticalContentAlignment="Top">
|
|
||||||
<ItemsControl x:Name="pluginsItemsControl" ItemsSource="{Binding SelectedLayout.Plugins}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<ui:CardExpander Tag="{Binding}" Margin="0 0 0 5" Expanded="OptionsCardExpander_Expanded">
|
|
||||||
<ui:CardExpander.Header>
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<ui:TextBlock Grid.Row="0" Grid.Column="0" FontTypography="Body" Text="{Binding Value.Metadata.Name}" />
|
|
||||||
<Label Grid.Row="1" Grid.Column="0" Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}" FontSize="12" ContentStringFormat="{DynamicResource authorFormat}">
|
|
||||||
<Label.Style>
|
|
||||||
<Style TargetType="Label" BasedOn="{StaticResource {x:Type Label}}">
|
|
||||||
<Setter Property="Content" Value="{Binding Value.Metadata.Author, Converter={StaticResource UnderscoreEscapingConverter}}" />
|
|
||||||
<Style.Triggers>
|
|
||||||
<DataTrigger Binding="{Binding Value.Metadata.Author}" Value="{x:Null}">
|
|
||||||
<Setter Property="Content" Value="{DynamicResource unknown}" />
|
|
||||||
</DataTrigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
</Label.Style>
|
|
||||||
</Label>
|
|
||||||
<ui:ToggleSwitch Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Click="PluginCheckBox_Click" IsChecked="{Binding Value.Enabled}" Tag="{Binding Key}" Margin="0,0,16,0" VerticalAlignment="Center" />
|
|
||||||
</Grid>
|
|
||||||
</ui:CardExpander.Header>
|
|
||||||
</ui:CardExpander>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</ScrollViewer>
|
|
||||||
|
|
||||||
<ui:Card Grid.Row="2" Grid.ColumnSpan="2" Margin="0 5 0 0" Padding="5">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="auto" />
|
|
||||||
<RowDefinition Height="5" />
|
|
||||||
<RowDefinition Height="auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="auto" />
|
|
||||||
<ColumnDefinition Width="5" />
|
|
||||||
<ColumnDefinition Width="1*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<ui:Button x:Name="screenSelectorButton" VerticalAlignment="Stretch" Padding="10,4" Click="ScreenSelectorButton_Click">
|
|
||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
|
||||||
<TextBlock x:Name="screenSelectorButtonText" VerticalAlignment="Center" Text="{Binding SelectedScreen.DisplayName}" />
|
|
||||||
<ui:SymbolIcon Symbol="ChevronDown24" Margin="8,0,0,0" FontSize="12" />
|
|
||||||
</StackPanel>
|
|
||||||
</ui:Button>
|
|
||||||
|
|
||||||
<ComboBox x:Name="layoutsComboBox" Grid.Column="2" ItemsSource="{Binding Settings.Layouts}" DisplayMemberPath="Name" SelectedValue="{Binding SelectedLayoutName}" SelectedValuePath="Name" VerticalAlignment="Center" SelectionChanged="LayoutsComboBox_SelectionChanged" />
|
|
||||||
|
|
||||||
<Grid Grid.Row="2" Grid.ColumnSpan="3">
|
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition />
|
||||||
|
<ColumnDefinition Width="10" />
|
||||||
|
<ColumnDefinition />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ui:ToggleSwitch x:Name="editCheckBox" Height="30" VerticalAlignment="Center" Margin="5 0 0 0" Content="{DynamicResource editLayout}" Click="EditCheckBox_Click" />
|
||||||
|
<ui:TextBox Grid.Column="2" Text="{Binding PluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="AllPluginsSearchTextBox_TextChanged" PlaceholderText="{DynamicResource searchInstalledPlugins}" Icon="{ui:SymbolIcon Search24}" IconPlacement="Right" />
|
||||||
|
</Grid>
|
||||||
|
</ui:Card>
|
||||||
|
|
||||||
|
<ScrollViewer Background="#FFBBBBBB" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel" Grid.Row="1" VerticalAlignment="Stretch" VerticalContentAlignment="Top">
|
||||||
|
<ItemsControl x:Name="pluginsItemsControl" ItemsSource="{Binding SelectedLayout.Plugins}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<ui:CardExpander Tag="{Binding}" Margin="0 0 0 5" Expanded="OptionsCardExpander_Expanded">
|
||||||
|
<ui:CardExpander.Header>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ui:TextBlock Grid.Row="0" Grid.Column="0" FontTypography="Body" Text="{Binding Value.Metadata.Name}" />
|
||||||
|
<Label Grid.Row="1" Grid.Column="0" Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}" FontSize="12" ContentStringFormat="{DynamicResource authorFormat}">
|
||||||
|
<Label.Style>
|
||||||
|
<Style TargetType="Label" BasedOn="{StaticResource {x:Type Label}}">
|
||||||
|
<Setter Property="Content" Value="{Binding Value.Metadata.Author, Converter={StaticResource UnderscoreEscapingConverter}}" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding Value.Metadata.Author}" Value="{x:Null}">
|
||||||
|
<Setter Property="Content" Value="{DynamicResource unknown}" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Label.Style>
|
||||||
|
</Label>
|
||||||
|
<ui:ToggleSwitch Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Click="PluginCheckBox_Click" IsChecked="{Binding Value.Enabled}" Tag="{Binding Key}" Margin="0,0,16,0" VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</ui:CardExpander.Header>
|
||||||
|
</ui:CardExpander>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<ui:Card Grid.Row="2" Grid.ColumnSpan="2" Margin="0 5 0 0" Padding="5">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="auto" />
|
||||||
|
<RowDefinition Height="5" />
|
||||||
|
<RowDefinition Height="auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="auto" />
|
||||||
|
<ColumnDefinition Width="5" />
|
||||||
<ColumnDefinition Width="1*" />
|
<ColumnDefinition Width="1*" />
|
||||||
<ColumnDefinition Width="5" />
|
|
||||||
<ColumnDefinition Width="0.5*" />
|
|
||||||
<ColumnDefinition Width="5" />
|
|
||||||
<ColumnDefinition Width="0.5*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<ui:Button x:Name="newLayoutButton" Content="{DynamicResource newLayout}" Icon="{ui:SymbolIcon Add24}" Grid.Column="2" HorizontalAlignment="Stretch" Click="NewLayoutButton_Click" FontWeight="Regular" />
|
<ui:Button x:Name="screenSelectorButton" VerticalAlignment="Stretch" Padding="10,4" Click="ScreenSelectorButton_Click">
|
||||||
<ui:Button x:Name="removeLayoutButton" Content="{DynamicResource deleteLayout}" Icon="{ui:SymbolIcon Delete24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="RemoveLayoutButton_Click" FontWeight="Regular" />
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock x:Name="screenSelectorButtonText" VerticalAlignment="Center" Text="{Binding SelectedScreen.DisplayName}" />
|
||||||
|
<ui:SymbolIcon Symbol="ChevronDown24" Margin="8,0,0,0" FontSize="12" />
|
||||||
|
</StackPanel>
|
||||||
|
</ui:Button>
|
||||||
|
|
||||||
|
<ComboBox x:Name="layoutsComboBox" Grid.Column="2" ItemsSource="{Binding Settings.Layouts}" DisplayMemberPath="Name" SelectedValue="{Binding SelectedLayoutName}" SelectedValuePath="Name" VerticalAlignment="Center" SelectionChanged="LayoutsComboBox_SelectionChanged" />
|
||||||
|
|
||||||
|
<Grid Grid.Row="2" Grid.ColumnSpan="3">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="1*" />
|
||||||
|
<ColumnDefinition Width="5" />
|
||||||
|
<ColumnDefinition Width="0.5*" />
|
||||||
|
<ColumnDefinition Width="5" />
|
||||||
|
<ColumnDefinition Width="0.5*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<ui:Button x:Name="shareLayoutButton" Icon="{ui:SymbolIcon Share24}" Grid.Column="0" HorizontalAlignment="Stretch" Click="ShareLayoutButton_Click" FontWeight="Regular">
|
||||||
|
<TextBlock>
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Text" Value="{DynamicResource shareOnModIo}" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding SelectedLayout.ShareState}" Value="Modified">
|
||||||
|
<Setter Property="Text" Value="{DynamicResource updateOnModIo}" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding SelectedLayout.ShareState}" Value="UpToDate">
|
||||||
|
<Setter Property="Text" Value="{DynamicResource sharedOnModIo}" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding SelectedLayout.ShareState}" Value="NotOwner">
|
||||||
|
<Setter Property="Text" Value="{DynamicResource shareCopyOnModIo}" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</ui:Button>
|
||||||
|
<ui:Button x:Name="newLayoutButton" Content="{DynamicResource newLayout}" Icon="{ui:SymbolIcon Add24}" Grid.Column="2" HorizontalAlignment="Stretch" Click="NewLayoutButton_Click" FontWeight="Regular" />
|
||||||
|
<ui:Button x:Name="removeLayoutButton" Content="{DynamicResource deleteLayout}" Icon="{ui:SymbolIcon Delete24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="RemoveLayoutButton_Click" FontWeight="Regular" />
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</ui:Card>
|
||||||
</ui:Card>
|
</Grid>
|
||||||
</Grid>
|
</busyIndicator:BusyMask>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public partial class MainPage : Page
|
|||||||
// Subscribe to manager events
|
// Subscribe to manager events
|
||||||
_manager.PluginsChanged += OnPluginsChanged;
|
_manager.PluginsChanged += OnPluginsChanged;
|
||||||
_manager.EditModeChanged += OnEditModeChanged;
|
_manager.EditModeChanged += OnEditModeChanged;
|
||||||
|
_manager.SettingsChanged += OnSettingsChanged;
|
||||||
|
|
||||||
Loaded += MainPage_Loaded;
|
Loaded += MainPage_Loaded;
|
||||||
Unloaded += MainPage_Unloaded;
|
Unloaded += MainPage_Unloaded;
|
||||||
@@ -46,6 +47,7 @@ public partial class MainPage : Page
|
|||||||
editCheckBox.IsChecked = _manager.IsEditMode;
|
editCheckBox.IsChecked = _manager.IsEditMode;
|
||||||
|
|
||||||
_dataContext.RefreshScreens();
|
_dataContext.RefreshScreens();
|
||||||
|
RefreshShareState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MainPage_Unloaded(object sender, RoutedEventArgs e)
|
private void MainPage_Unloaded(object sender, RoutedEventArgs e)
|
||||||
@@ -53,6 +55,7 @@ public partial class MainPage : Page
|
|||||||
// Unsubscribe from events
|
// Unsubscribe from events
|
||||||
_manager.PluginsChanged -= OnPluginsChanged;
|
_manager.PluginsChanged -= OnPluginsChanged;
|
||||||
_manager.EditModeChanged -= OnEditModeChanged;
|
_manager.EditModeChanged -= OnEditModeChanged;
|
||||||
|
_manager.SettingsChanged -= OnSettingsChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPluginsChanged()
|
private void OnPluginsChanged()
|
||||||
@@ -61,9 +64,20 @@ public partial class MainPage : Page
|
|||||||
{
|
{
|
||||||
_dataContext.Settings = _manager.Settings;
|
_dataContext.Settings = _manager.Settings;
|
||||||
ApplyPluginsFilter();
|
ApplyPluginsFilter();
|
||||||
|
RefreshShareState();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnSettingsChanged()
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(RefreshShareState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshShareState()
|
||||||
|
{
|
||||||
|
ModIoService.RefreshShareState(_manager.SelectedLayout);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnEditModeChanged(bool editMode)
|
private void OnEditModeChanged(bool editMode)
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() =>
|
Dispatcher.Invoke(() =>
|
||||||
@@ -157,6 +171,21 @@ public partial class MainPage : Page
|
|||||||
private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
ApplySelectedLayout();
|
ApplySelectedLayout();
|
||||||
|
RefreshShareState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ShareLayoutButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
shareLayoutButton.IsEnabled = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ModIoService.ShareAsync(_manager.SelectedLayout, Window.GetWindow(this), isBusy => _dataContext.IsSharing = isBusy);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
shareLayoutButton.IsEnabled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -273,21 +302,10 @@ public partial class MainPage : Page
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = _manager.Settings.Layouts.Remove(layout);
|
// A subscribed layout would be downloaded again on the next sync.
|
||||||
|
await ModIoService.UnsubscribeAsync(layout);
|
||||||
|
|
||||||
// Remove any screen bindings pointing to the deleted layout
|
_manager.RemoveLayout(layout);
|
||||||
List<string> boundScreens = _manager.Settings.ScreenLayouts
|
|
||||||
.Where(kvp => kvp.Value == layout.Name)
|
|
||||||
.Select(kvp => kvp.Key)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (string screenId in boundScreens)
|
|
||||||
{
|
|
||||||
_ = _manager.Settings.ScreenLayouts.Remove(screenId);
|
|
||||||
}
|
|
||||||
|
|
||||||
_manager.SaveSettings();
|
|
||||||
_manager.LoadLayout();
|
|
||||||
_dataContext.RefreshSelection();
|
_dataContext.RefreshSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||||
|
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:local="clr-namespace:DesktopMagic.Pages"
|
xmlns:local="clr-namespace:DesktopMagic.Pages"
|
||||||
@@ -9,44 +10,66 @@
|
|||||||
d:DesignHeight="450" d:DesignWidth="800"
|
d:DesignHeight="450" d:DesignWidth="800"
|
||||||
Title="ThemePage">
|
Title="ThemePage">
|
||||||
|
|
||||||
<Grid Margin="{StaticResource DefaultMargin}">
|
<busyIndicator:BusyMask IsBusy="{Binding IsSharing}" IndicatorType="ThreeDots" BusyContent="{DynamicResource uploadingToModIo}" BusyContentMargin="0,20,0,0" Foreground="{DynamicResource TextFillColorPrimaryBrush}" Background="{DynamicResource ControlStrokeColorDefaultBrush}">
|
||||||
<Grid.RowDefinitions>
|
<Grid Margin="{StaticResource DefaultMargin}">
|
||||||
<RowDefinition Height="*" />
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="auto" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
<RowDefinition Height="auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<ui:ListView x:Name="themesListBox" Grid.Row="0" Grid.ColumnSpan="1" ItemsSource="{Binding Settings.Themes}" SelectedValue="{Binding SelectedLayout.CurrentThemeName}" SelectedValuePath="Name" SelectedIndex="2" SelectionMode="Single">
|
<ui:ListView x:Name="themesListBox" Grid.Row="0" Grid.ColumnSpan="1" ItemsSource="{Binding Settings.Themes}" SelectedValue="{Binding SelectedLayout.CurrentThemeName}" SelectedValuePath="Name" SelectedIndex="2" SelectionMode="Single">
|
||||||
<ui:ListView.ItemTemplate>
|
<ui:ListView.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBlock Margin="8" VerticalAlignment="Center" FontWeight="SemiBold" Text="{Binding Name, Mode=OneWay}" />
|
<TextBlock Margin="8" VerticalAlignment="Center" FontWeight="SemiBold" Text="{Binding Name, Mode=OneWay}" />
|
||||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal" Margin="{StaticResource DefaultMargin}">
|
<StackPanel Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal" Margin="{StaticResource DefaultMargin}">
|
||||||
<ui:Button x:Name="editThemeButton" Content="{DynamicResource editTheme}" Icon="{ui:SymbolIcon Color24}" HorizontalAlignment="Stretch" Click="EditThemeButton_Click" FontWeight="Regular" Tag="{Binding}">
|
<ui:Button x:Name="shareThemeButton" Icon="{ui:SymbolIcon Share24}" Margin="0 0 5 0" Click="ShareThemeButton_Click" FontWeight="Regular" Tag="{Binding}">
|
||||||
|
<TextBlock>
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Text" Value="{DynamicResource shareOnModIo}" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding ShareState}" Value="Modified">
|
||||||
|
<Setter Property="Text" Value="{DynamicResource updateOnModIo}" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding ShareState}" Value="UpToDate">
|
||||||
|
<Setter Property="Text" Value="{DynamicResource sharedOnModIo}" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding ShareState}" Value="NotOwner">
|
||||||
|
<Setter Property="Text" Value="{DynamicResource shareCopyOnModIo}" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</ui:Button>
|
||||||
|
<ui:Button x:Name="editThemeButton" Content="{DynamicResource editTheme}" Icon="{ui:SymbolIcon Color24}" HorizontalAlignment="Stretch" Click="EditThemeButton_Click" FontWeight="Regular" Tag="{Binding}">
|
||||||
|
|
||||||
</ui:Button>
|
</ui:Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ui:ListView.ItemTemplate>
|
</ui:ListView.ItemTemplate>
|
||||||
</ui:ListView>
|
</ui:ListView>
|
||||||
|
|
||||||
<ui:Card Grid.Row="1" Grid.ColumnSpan="2" Margin="0 5 0 0" Padding="5">
|
<ui:Card Grid.Row="1" Grid.ColumnSpan="2" Margin="0 5 0 0" Padding="5">
|
||||||
<Grid >
|
<Grid >
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="1*" />
|
<ColumnDefinition Width="1*" />
|
||||||
<ColumnDefinition Width="5" />
|
<ColumnDefinition Width="5" />
|
||||||
<ColumnDefinition Width="0.5*" />
|
<ColumnDefinition Width="0.5*" />
|
||||||
<ColumnDefinition Width="5" />
|
<ColumnDefinition Width="5" />
|
||||||
<ColumnDefinition Width="0.5*" />
|
<ColumnDefinition Width="0.5*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<ui:Button x:Name="newThemeButton" Content="{DynamicResource newTheme}" Icon="{ui:SymbolIcon Add24}" Grid.Column="2" HorizontalAlignment="Stretch" Click="AddThemeButton_Click" FontWeight="Regular"/>
|
<ui:Button x:Name="newThemeButton" Content="{DynamicResource newTheme}" Icon="{ui:SymbolIcon Add24}" Grid.Column="2" HorizontalAlignment="Stretch" Click="AddThemeButton_Click" FontWeight="Regular"/>
|
||||||
<ui:Button x:Name="deleteThemeButton" Content="{DynamicResource deleteTheme}" Icon="{ui:SymbolIcon Delete24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="DeleteThemeButton_Click" FontWeight="Regular"/>
|
<ui:Button x:Name="deleteThemeButton" Content="{DynamicResource deleteTheme}" Icon="{ui:SymbolIcon Delete24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="DeleteThemeButton_Click" FontWeight="Regular"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ui:Card>
|
</ui:Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</busyIndicator:BusyMask>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using DesktopMagic.DataContexts;
|
using DesktopMagic.DataContexts;
|
||||||
using DesktopMagic.Dialogs;
|
using DesktopMagic.Dialogs;
|
||||||
|
using DesktopMagic.Helpers;
|
||||||
using DesktopMagic.Plugins;
|
using DesktopMagic.Plugins;
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -30,9 +31,15 @@ public partial class ThemePage : Page
|
|||||||
// Subscribe to manager events
|
// Subscribe to manager events
|
||||||
_manager.SettingsChanged += OnSettingsChanged;
|
_manager.SettingsChanged += OnSettingsChanged;
|
||||||
|
|
||||||
|
Loaded += ThemePage_Loaded;
|
||||||
Unloaded += ThemePage_Unloaded;
|
Unloaded += ThemePage_Unloaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ThemePage_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
RefreshShareStates();
|
||||||
|
}
|
||||||
|
|
||||||
private void ThemePage_Unloaded(object sender, RoutedEventArgs e)
|
private void ThemePage_Unloaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// Unsubscribe from events
|
// Unsubscribe from events
|
||||||
@@ -44,9 +51,37 @@ public partial class ThemePage : Page
|
|||||||
Dispatcher.Invoke(() =>
|
Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
_dataContext.Settings = _manager.Settings;
|
_dataContext.Settings = _manager.Settings;
|
||||||
|
RefreshShareStates();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RefreshShareStates()
|
||||||
|
{
|
||||||
|
foreach (Theme theme in _manager.Settings.Themes)
|
||||||
|
{
|
||||||
|
ModIoService.RefreshShareState(theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ShareThemeButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not System.Windows.Controls.Button button || button.Tag is not Theme theme)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.IsEnabled = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ModIoService.ShareAsync(theme, Window.GetWindow(this), isBusy => _dataContext.IsSharing = isBusy);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
button.IsEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async void AddThemeButton_Click(object sender, RoutedEventArgs e)
|
private async void AddThemeButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
InputDialog inputDialog = new((string)FindResource("enterThemeName"))
|
InputDialog inputDialog = new((string)FindResource("enterThemeName"))
|
||||||
@@ -104,6 +139,9 @@ public partial class ThemePage : Page
|
|||||||
|
|
||||||
if (themesListBox.SelectedItem is Theme theme)
|
if (themesListBox.SelectedItem is Theme theme)
|
||||||
{
|
{
|
||||||
|
// A subscribed theme would be downloaded again on the next sync.
|
||||||
|
await ModIoService.UnsubscribeAsync(theme);
|
||||||
|
|
||||||
_ = _manager.Settings.Themes.Remove(theme);
|
_ = _manager.Settings.Themes.Remove(theme);
|
||||||
_manager.SaveSettings();
|
_manager.SaveSettings();
|
||||||
}
|
}
|
||||||
@@ -122,5 +160,6 @@ public partial class ThemePage : Page
|
|||||||
};
|
};
|
||||||
|
|
||||||
_ = themeDialog.ShowDialog();
|
_ = themeDialog.ShowDialog();
|
||||||
|
ModIoService.RefreshShareState(theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,19 +31,26 @@
|
|||||||
<ui:Card Grid.ColumnSpan="3" Padding="5" Margin="0 0 0 5">
|
<ui:Card Grid.ColumnSpan="3" Padding="5" Margin="0 0 0 5">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="auto" />
|
||||||
|
<ColumnDefinition Width="10" />
|
||||||
<ColumnDefinition />
|
<ColumnDefinition />
|
||||||
<ColumnDefinition Width="10" />
|
<ColumnDefinition Width="10" />
|
||||||
<ColumnDefinition />
|
<ColumnDefinition />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<ui:TextBox Grid.Column="0" Text="{Binding AllPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="AllPluginsSearchTextBox_TextChanged" PlaceholderText="{DynamicResource searchAllPlugins}" Icon="{ui:SymbolIcon Search24}" IconPlacement="Right" />
|
<ComboBox x:Name="categoryComboBox" Grid.Column="0" MinWidth="130" VerticalAlignment="Center" SelectedIndex="{Binding CategoryIndex, Mode=TwoWay}" SelectionChanged="CategoryComboBox_SelectionChanged">
|
||||||
<ui:TextBox Grid.Column="2" Text="{Binding InstalledPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="InstalledPluginsSearchTextBox_TextChanged" PlaceholderText="{DynamicResource searchInstalledPlugins}" Icon="{ui:SymbolIcon Search24}" IconPlacement="Right" />
|
<ComboBoxItem Content="{DynamicResource plugins}" />
|
||||||
|
<ComboBoxItem Content="{DynamicResource layouts}" />
|
||||||
|
<ComboBoxItem Content="{DynamicResource themes}" />
|
||||||
|
</ComboBox>
|
||||||
|
<ui:TextBox Grid.Column="2" Text="{Binding AllPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="AllPluginsSearchTextBox_TextChanged" PlaceholderText="{DynamicResource searchModIo}" Icon="{ui:SymbolIcon Search24}" IconPlacement="Right" />
|
||||||
|
<ui:TextBox Grid.Column="4" Text="{Binding InstalledPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="InstalledPluginsSearchTextBox_TextChanged" PlaceholderText="{DynamicResource searchInstalled}" Icon="{ui:SymbolIcon Search24}" IconPlacement="Right" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</ui:Card>
|
</ui:Card>
|
||||||
|
|
||||||
<busyIndicator:BusyMask Grid.Row="2" IsBusy="{Binding IsSearching}" IndicatorType="ThreeDots" BusyContent="Searching..." BusyContentMargin="0,50,0,0" Foreground="{DynamicResource TextFillColorPrimaryBrush}" Background="{DynamicResource ControlStrokeColorDefaultBrush}">
|
<busyIndicator:BusyMask Grid.Row="2" IsBusy="{Binding IsSearching}" IndicatorType="ThreeDots" BusyContent="Searching..." BusyContentMargin="0,50,0,0" Foreground="{DynamicResource TextFillColorPrimaryBrush}" Background="{DynamicResource ControlStrokeColorDefaultBrush}">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto" VerticalAlignment="Stretch" Padding="0" VerticalContentAlignment="Top" Margin="0 0 2.5 0">
|
<ScrollViewer VerticalScrollBarVisibility="Auto" VerticalAlignment="Stretch" Padding="0" VerticalContentAlignment="Top" Margin="0 0 2.5 0">
|
||||||
<ItemsControl ItemsSource="{Binding AllPlugins}">
|
<ItemsControl ItemsSource="{Binding ShownAll}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<local:ModEntry Margin="0 0 0 5" />
|
<local:ModEntry Margin="0 0 0 5" />
|
||||||
@@ -56,7 +63,7 @@
|
|||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate>
|
<ControlTemplate>
|
||||||
<TextBlock Text="No plugins found!" FontSize="20" VerticalAlignment="Center" HorizontalAlignment="Center" />
|
<TextBlock Text="{DynamicResource nothingFound}" FontSize="20" VerticalAlignment="Center" HorizontalAlignment="Center" />
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
@@ -69,7 +76,7 @@
|
|||||||
</busyIndicator:BusyMask>
|
</busyIndicator:BusyMask>
|
||||||
|
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Row="2" Grid.Column="2" VerticalAlignment="Stretch" Padding="0" VerticalContentAlignment="Top">
|
<ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Row="2" Grid.Column="2" VerticalAlignment="Stretch" Padding="0" VerticalContentAlignment="Top">
|
||||||
<ItemsControl ItemsSource="{Binding InstalledPlugins}" Margin="2.5 0 0 0">
|
<ItemsControl ItemsSource="{Binding ShownInstalled}" Margin="2.5 0 0 0">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<local:ModEntry Margin="0 0 0 5"/>
|
<local:ModEntry Margin="0 0 0 5"/>
|
||||||
@@ -82,7 +89,7 @@
|
|||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate>
|
<ControlTemplate>
|
||||||
<TextBlock Text="No plugins found!" FontSize="20" VerticalAlignment="Center" HorizontalAlignment="Center" />
|
<TextBlock Text="{DynamicResource nothingFound}" FontSize="20" VerticalAlignment="Center" HorizontalAlignment="Center" />
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using DesktopMagic.DataContexts;
|
using DesktopMagic.DataContexts;
|
||||||
using DesktopMagic.Dialogs;
|
using DesktopMagic.Dialogs;
|
||||||
using DesktopMagic.Helpers;
|
using DesktopMagic.Helpers;
|
||||||
|
using DesktopMagic.Settings;
|
||||||
|
|
||||||
using Modio;
|
using Modio;
|
||||||
using Modio.Filters;
|
using Modio.Filters;
|
||||||
@@ -32,8 +33,7 @@ namespace DesktopMagic.Plugins;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class PluginManager : Page
|
public partial class PluginManager : Page
|
||||||
{
|
{
|
||||||
private const int ModIoGameId = 5665;
|
private const uint ModIoGameId = ModIoService.GameId;
|
||||||
private const string ModIoApiKey = "88e6ea774c3a502b06114e7fee0829ac";
|
|
||||||
private readonly HttpClient httpClient = new();
|
private readonly HttpClient httpClient = new();
|
||||||
private readonly PluginManagerDataContext pluginManagerDataContext = new();
|
private readonly PluginManagerDataContext pluginManagerDataContext = new();
|
||||||
private readonly string pluginsPath = Path.Combine(App.ApplicationDataPath, "Plugins");
|
private readonly string pluginsPath = Path.Combine(App.ApplicationDataPath, "Plugins");
|
||||||
@@ -48,23 +48,16 @@ public partial class PluginManager : Page
|
|||||||
private bool changed = false;
|
private bool changed = false;
|
||||||
private Client modIoClient;
|
private Client modIoClient;
|
||||||
|
|
||||||
|
// mod.io data of the local layouts and themes linked to a mod, by mod ID.
|
||||||
|
private readonly Dictionary<uint, Mod> linkedMods = [];
|
||||||
|
|
||||||
public PluginManager()
|
public PluginManager()
|
||||||
{
|
{
|
||||||
Resources.MergedDictionaries.Add(App.LanguageDictionary);
|
Resources.MergedDictionaries.Add(App.LanguageDictionary);
|
||||||
|
|
||||||
string? modIoAccessToken = MainWindowDataContext.GetSettings().ModIoAccessToken;
|
modIoClient = ModIoService.CreateClient();
|
||||||
|
pluginManagerDataContext.IsAuthenticated = ModIoService.IsAuthenticated;
|
||||||
if (modIoAccessToken is null)
|
App.Logger.LogInfo($"Initialized mod.io client {(ModIoService.IsAuthenticated ? "with" : "without")} authentication", source: "PluginManager");
|
||||||
{
|
|
||||||
modIoClient = new Client(new Credentials(ModIoApiKey));
|
|
||||||
App.Logger.LogInfo("Initialized mod.io client without authentication", source: "PluginManager");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
modIoClient = new Client(new Credentials(ModIoApiKey, modIoAccessToken));
|
|
||||||
pluginManagerDataContext.IsAuthenticated = true;
|
|
||||||
App.Logger.LogInfo("Initialized mod.io client with authentication", source: "PluginManager");
|
|
||||||
}
|
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
@@ -72,7 +65,15 @@ public partial class PluginManager : Page
|
|||||||
searchTimer.Tick += async (sender, e) =>
|
searchTimer.Tick += async (sender, e) =>
|
||||||
{
|
{
|
||||||
searchTimer.Stop();
|
searchTimer.Stop();
|
||||||
await SearchAllPlugins(pluginManagerDataContext.AllPluginsSearchText);
|
|
||||||
|
if (pluginManagerDataContext.Category == ModCategory.Plugins)
|
||||||
|
{
|
||||||
|
await SearchAllPlugins(pluginManagerDataContext.AllPluginsSearchText);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await SearchShared(pluginManagerDataContext.AllPluginsSearchText);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Loaded += PluginManager_Loaded;
|
Loaded += PluginManager_Loaded;
|
||||||
@@ -222,7 +223,7 @@ public partial class PluginManager : Page
|
|||||||
IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
|
IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
|
||||||
await foreach (Mod mod in mods)
|
await foreach (Mod mod in mods)
|
||||||
{
|
{
|
||||||
if (pluginIds.Contains(mod.Id))
|
if (pluginIds.Contains(mod.Id) || !ModIoService.IsPluginMod(mod))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -237,6 +238,8 @@ public partial class PluginManager : Page
|
|||||||
}
|
}
|
||||||
|
|
||||||
await SyncPlugins();
|
await SyncPlugins();
|
||||||
|
await UpdateLinkedItems();
|
||||||
|
await PopulateShared();
|
||||||
|
|
||||||
pluginManagerDataContext.IsLoading = false;
|
pluginManagerDataContext.IsLoading = false;
|
||||||
App.Logger.LogInfo("Plugin Manager initialization complete", source: "PluginManager");
|
App.Logger.LogInfo("Plugin Manager initialization complete", source: "PluginManager");
|
||||||
@@ -353,20 +356,177 @@ public partial class PluginManager : Page
|
|||||||
|
|
||||||
private void InstalledPluginsSearchTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
private void InstalledPluginsSearchTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(pluginManagerDataContext.InstalledPluginsSearchText))
|
FilterInstalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FilterInstalled()
|
||||||
|
{
|
||||||
|
string searchText = pluginManagerDataContext.InstalledPluginsSearchText;
|
||||||
|
|
||||||
|
foreach (PluginEntryDataContext pluginEntryDataContext in pluginManagerDataContext.ShownInstalled)
|
||||||
{
|
{
|
||||||
foreach (PluginEntryDataContext pluginEntryDataContext in pluginManagerDataContext.InstalledPlugins)
|
pluginEntryDataContext.IsVisible = string.IsNullOrWhiteSpace(searchText) || pluginEntryDataContext.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void CategoryComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!IsLoaded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginManagerDataContext.CategoryIndex = categoryComboBox.SelectedIndex;
|
||||||
|
pluginManagerDataContext.IsLoading = true;
|
||||||
|
await PopulateShared();
|
||||||
|
FilterInstalled();
|
||||||
|
pluginManagerDataContext.IsLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches the mods of all linked layouts and themes and applies newer versions.
|
||||||
|
/// Subscribed items are also updated by the subscription sync, this covers items installed while logged out.
|
||||||
|
/// </summary>
|
||||||
|
private async Task UpdateLinkedItems()
|
||||||
|
{
|
||||||
|
linkedMods.Clear();
|
||||||
|
|
||||||
|
foreach (IModIoShareable item in ModIoService.GetSharedItems().Where(item => item.ModIo is not null).ToList())
|
||||||
|
{
|
||||||
|
uint modId = item.ModIo!.ModId;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
pluginEntryDataContext.IsVisible = true;
|
Mod mod = await modIoClient.Games[ModIoGameId].Mods[modId].Get();
|
||||||
|
linkedMods[modId] = mod;
|
||||||
|
|
||||||
|
if (await ModIoService.UpdateAsync(item, mod, EnsurePluginInstalled))
|
||||||
|
{
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogError($"Failed to check \"{item.Name}\" (mod {modId}) for updates: {ex.Message}", source: "PluginManager");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fills the lists of the selected layout or theme category.
|
||||||
|
/// </summary>
|
||||||
|
private async Task PopulateShared()
|
||||||
|
{
|
||||||
|
pluginManagerDataContext.InstalledShared.Clear();
|
||||||
|
pluginManagerDataContext.AllShared.Clear();
|
||||||
|
|
||||||
|
if (pluginManagerDataContext.Category == ModCategory.Plugins)
|
||||||
{
|
{
|
||||||
foreach (PluginEntryDataContext pluginEntryDataContext in pluginManagerDataContext.InstalledPlugins)
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable<IModIoShareable> items = pluginManagerDataContext.Category == ModCategory.Layouts
|
||||||
|
? _manager.Settings.Layouts
|
||||||
|
: _manager.Settings.Themes;
|
||||||
|
|
||||||
|
foreach (IModIoShareable item in items.Where(item => item.ModIo is not null).ToList())
|
||||||
|
{
|
||||||
|
uint modId = item.ModIo!.ModId;
|
||||||
|
|
||||||
|
if (!linkedMods.TryGetValue(modId, out Mod? mod))
|
||||||
{
|
{
|
||||||
pluginEntryDataContext.IsVisible = pluginEntryDataContext.Name.Contains(pluginManagerDataContext.InstalledPluginsSearchText, StringComparison.OrdinalIgnoreCase);
|
try
|
||||||
|
{
|
||||||
|
mod = await modIoClient.Games[ModIoGameId].Mods[modId].Get();
|
||||||
|
linkedMods[modId] = mod;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogError($"Failed to fetch mod {modId} of \"{item.Name}\": {ex.Message}", source: "PluginManager");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the local name, it can differ from the mod name (e.g. "Default (2)").
|
||||||
|
PluginMetadata metadata = mod is null ? new PluginMetadata(item.Name, modId) : new PluginMetadata(mod) { Name = item.Name };
|
||||||
|
pluginManagerDataContext.InstalledShared.Add(new PluginEntryDataContext(metadata, new CommandHandler(async () => await RemoveShared(item)), PluginEntryDataContext.Mode.Uninstall));
|
||||||
|
}
|
||||||
|
|
||||||
|
await SearchShared(pluginManagerDataContext.AllPluginsSearchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SearchShared(string searchString)
|
||||||
|
{
|
||||||
|
pluginManagerDataContext.AllShared.Clear();
|
||||||
|
|
||||||
|
if (pluginManagerDataContext.Category == ModCategory.Plugins)
|
||||||
|
{
|
||||||
|
pluginManagerDataContext.IsSearching = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string tag = pluginManagerDataContext.Category == ModCategory.Layouts ? SharePackage.LayoutTag : SharePackage.ThemeTag;
|
||||||
|
Filter filter = ModFilter.Tags.Eq(tag);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(searchString))
|
||||||
|
{
|
||||||
|
if (!searchString.Contains('*'))
|
||||||
|
{
|
||||||
|
searchString = $"*{searchString.Trim()}*";
|
||||||
|
}
|
||||||
|
|
||||||
|
filter = filter.And(ModFilter.Name.Like(searchString));
|
||||||
|
}
|
||||||
|
|
||||||
|
filter = filter.And(ModFilter.Popular.Desc()).Limit(100);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
App.Logger.LogInfo($"Fetching shared {tag} mods from mod.io", source: "PluginManager");
|
||||||
|
IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
|
||||||
|
await foreach (Mod mod in mods)
|
||||||
|
{
|
||||||
|
if (ModIoService.FindLinkedItem(mod.Id) is not null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginManagerDataContext.AllShared.Add(new PluginEntryDataContext(new(mod), new CommandHandler(async () => await InstallShared(mod)), PluginEntryDataContext.Mode.Install));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogError($"Failed to fetch shared {tag} mods from mod.io: {ex.Message}", source: "PluginManager");
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginManagerDataContext.IsSearching = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InstallShared(Mod mod)
|
||||||
|
{
|
||||||
|
pluginManagerDataContext.IsLoading = true;
|
||||||
|
|
||||||
|
if (await ModIoService.InstallAsync(mod, EnsurePluginInstalled))
|
||||||
|
{
|
||||||
|
linkedMods[mod.Id] = mod;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await PopulateShared();
|
||||||
|
FilterInstalled();
|
||||||
|
pluginManagerDataContext.IsLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RemoveShared(IModIoShareable item)
|
||||||
|
{
|
||||||
|
if (!await ModIoService.RemoveAsync(item))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginManagerDataContext.IsLoading = true;
|
||||||
|
await PopulateShared();
|
||||||
|
FilterInstalled();
|
||||||
|
pluginManagerDataContext.IsLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SearchAllPlugins(string searchString)
|
private async Task SearchAllPlugins(string searchString)
|
||||||
@@ -386,7 +546,7 @@ public partial class PluginManager : Page
|
|||||||
IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
|
IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
|
||||||
await foreach (Mod mod in mods)
|
await foreach (Mod mod in mods)
|
||||||
{
|
{
|
||||||
if (pluginManagerDataContext.InstalledPlugins.Any(p => p.Id == mod.Id))
|
if (pluginManagerDataContext.InstalledPlugins.Any(p => p.Id == mod.Id) || !ModIoService.IsPluginMod(mod))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -674,52 +834,21 @@ public class {pluginSafeName}Plugin : Plugin
|
|||||||
{
|
{
|
||||||
if (pluginManagerDataContext.IsAuthenticated)
|
if (pluginManagerDataContext.IsAuthenticated)
|
||||||
{
|
{
|
||||||
App.Logger.LogInfo("Logging out from mod.io", source: "PluginManager");
|
ModIoService.LogOut();
|
||||||
MainWindowDataContext.GetSettings().ModIoAccessToken = null;
|
modIoClient = ModIoService.CreateClient();
|
||||||
pluginManagerDataContext.IsAuthenticated = false;
|
pluginManagerDataContext.IsAuthenticated = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
App.Logger.LogInfo("Starting mod.io authentication", source: "PluginManager");
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
InputDialog inputDialog = new((string)FindResource("enterModIoEmail"), "Plugin Manager")
|
if (!await ModIoService.LogInAsync(Window.GetWindow(this), "Plugin Manager"))
|
||||||
{
|
{
|
||||||
Owner = Window.GetWindow(this),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (inputDialog.ShowDialog() != true)
|
|
||||||
{
|
|
||||||
App.Logger.LogInfo("Authentication cancelled by user", source: "PluginManager");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
App.Logger.LogInfo($"Requesting authentication code for email: {inputDialog.ResponseText}", source: "PluginManager");
|
|
||||||
await modIoClient.Auth.RequestCode(ModIoApiKey, inputDialog.ResponseText);
|
|
||||||
|
|
||||||
inputDialog = new((string)FindResource("enterModIoAccessToken"), "Plugin Manager")
|
|
||||||
{
|
|
||||||
Owner = Window.GetWindow(this),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (inputDialog.ShowDialog() != true)
|
|
||||||
{
|
|
||||||
App.Logger.LogInfo("Authentication cancelled by user", source: "PluginManager");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginManagerDataContext.IsLoading = true;
|
pluginManagerDataContext.IsLoading = true;
|
||||||
|
modIoClient = ModIoService.CreateClient();
|
||||||
AccessToken accessToken = await modIoClient.Auth.SecurityCode(ModIoApiKey, inputDialog.ResponseText);
|
|
||||||
|
|
||||||
if (accessToken.Value is not null)
|
|
||||||
{
|
|
||||||
modIoClient = new Client(new Credentials(ModIoApiKey, accessToken.Value));
|
|
||||||
App.Logger.LogInfo("Successfully authenticated with mod.io", source: "PluginManager");
|
|
||||||
}
|
|
||||||
|
|
||||||
MainWindowDataContext.GetSettings().ModIoAccessToken = accessToken.Value;
|
|
||||||
pluginManagerDataContext.IsAuthenticated = true;
|
pluginManagerDataContext.IsAuthenticated = true;
|
||||||
|
|
||||||
App.Logger.LogInfo("Subscribing to installed plugins on mod.io", source: "PluginManager");
|
App.Logger.LogInfo("Subscribing to installed plugins on mod.io", source: "PluginManager");
|
||||||
@@ -739,7 +868,9 @@ public class {pluginSafeName}Plugin : Plugin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ModIoService.SubscribeToLinkedItemsAsync();
|
||||||
await SyncPlugins();
|
await SyncPlugins();
|
||||||
|
await PopulateShared();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -767,7 +898,8 @@ public class {pluginSafeName}Plugin : Plugin
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IReadOnlyList<Mod> mods = await modIoClient.User.GetSubscriptions(ModFilter.GameId.Eq(ModIoGameId)).ToList();
|
IReadOnlyList<Mod> subscriptions = await modIoClient.User.GetSubscriptions(ModFilter.GameId.Eq(ModIoGameId)).ToList();
|
||||||
|
List<Mod> mods = subscriptions.Where(ModIoService.IsPluginMod).ToList();
|
||||||
App.Logger.LogInfo($"Found {mods.Count} subscribed plugins on mod.io", source: "PluginManager");
|
App.Logger.LogInfo($"Found {mods.Count} subscribed plugins on mod.io", source: "PluginManager");
|
||||||
|
|
||||||
List<PluginEntryDataContext> pluginsToRemove = [];
|
List<PluginEntryDataContext> pluginsToRemove = [];
|
||||||
@@ -800,6 +932,12 @@ public class {pluginSafeName}Plugin : Plugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
App.Logger.LogInfo("Plugin sync completed", source: "PluginManager");
|
App.Logger.LogInfo("Plugin sync completed", source: "PluginManager");
|
||||||
|
|
||||||
|
// Shared layouts and themes are synced after the plugins, so the plugins they use are installed first.
|
||||||
|
if (await ModIoService.SyncSubscriptionsAsync(subscriptions, EnsurePluginInstalled))
|
||||||
|
{
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -807,6 +945,19 @@ public class {pluginSafeName}Plugin : Plugin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task EnsurePluginInstalled(uint pluginId)
|
||||||
|
{
|
||||||
|
if (pluginManagerDataContext.InstalledPlugins.Any(p => p.Id == pluginId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
App.Logger.LogInfo($"Installing plugin {pluginId} required by a shared layout", source: "PluginManager");
|
||||||
|
Mod mod = await modIoClient.Games[ModIoGameId].Mods[pluginId].Get();
|
||||||
|
await Install(mod);
|
||||||
|
pluginManagerDataContext.IsLoading = true;
|
||||||
|
}
|
||||||
|
|
||||||
private string GetCsprojPath(string pluginName)
|
private string GetCsprojPath(string pluginName)
|
||||||
{
|
{
|
||||||
string pluginSafeName = GetPluginSafeName(pluginName);
|
string pluginSafeName = GetPluginSafeName(pluginName);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace DesktopMagic.Plugins;
|
|||||||
public class SettingElement
|
public class SettingElement
|
||||||
{
|
{
|
||||||
private string jsonValue = string.Empty;
|
private string jsonValue = string.Empty;
|
||||||
|
private bool shareable;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Setting? Input { get; set; }
|
public Setting? Input { get; set; }
|
||||||
@@ -16,6 +17,17 @@ public class SettingElement
|
|||||||
|
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the value may be included when sharing a layout on mod.io.
|
||||||
|
/// Free-text values (URLs, locations, file paths, ...) can contain personal data and are never shared.
|
||||||
|
/// Persisted so the decision stays stable while the plugin is not loaded.
|
||||||
|
/// </summary>
|
||||||
|
public bool Shareable
|
||||||
|
{
|
||||||
|
get => Input is null ? shareable : IsShareableInput(Input);
|
||||||
|
set => shareable = value;
|
||||||
|
}
|
||||||
|
|
||||||
public string JsonValue
|
public string JsonValue
|
||||||
{
|
{
|
||||||
get => Input?.GetJsonValue() ?? jsonValue;
|
get => Input?.GetJsonValue() ?? jsonValue;
|
||||||
@@ -40,6 +52,11 @@ public class SettingElement
|
|||||||
OrderIndex = orderIndex;
|
OrderIndex = orderIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsShareableInput(Setting input)
|
||||||
|
{
|
||||||
|
return input is CheckBox or ColorPicker or ComboBox or IntegerUpDown or Slider;
|
||||||
|
}
|
||||||
|
|
||||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using DesktopMagic.Api;
|
using DesktopMagic.Api;
|
||||||
using DesktopMagic.Helpers;
|
using DesktopMagic.Helpers;
|
||||||
|
using DesktopMagic.Settings;
|
||||||
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
@@ -11,7 +12,7 @@ using Color = System.Drawing.Color;
|
|||||||
|
|
||||||
namespace DesktopMagic.Plugins;
|
namespace DesktopMagic.Plugins;
|
||||||
|
|
||||||
public class Theme(string name) : ITheme, INotifyPropertyChanged
|
public class Theme(string name) : ITheme, INotifyPropertyChanged, IModIoShareable
|
||||||
{
|
{
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
@@ -21,6 +22,23 @@ public class Theme(string name) : ITheme, INotifyPropertyChanged
|
|||||||
private string font = "Segoe UI";
|
private string font = "Segoe UI";
|
||||||
private int cornerRadius;
|
private int cornerRadius;
|
||||||
private int margin;
|
private int margin;
|
||||||
|
private ModIoShareState shareState;
|
||||||
|
|
||||||
|
public ModIoLink? ModIo { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public ModIoShareState ShareState
|
||||||
|
{
|
||||||
|
get => shareState;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (shareState != value)
|
||||||
|
{
|
||||||
|
shareState = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string Name
|
public string Name
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -70,6 +70,30 @@
|
|||||||
<system:String x:Key="releaseInfoPublishedFormat">Veröffentlicht: {0}</system:String>
|
<system:String x:Key="releaseInfoPublishedFormat">Veröffentlicht: {0}</system:String>
|
||||||
<system:String x:Key="releaseInfoBodyLabel">Release Notes:</system:String>
|
<system:String x:Key="releaseInfoBodyLabel">Release Notes:</system:String>
|
||||||
<system:String x:Key="openReleasePage">Release-Seite öffnen</system:String>
|
<system:String x:Key="openReleasePage">Release-Seite öffnen</system:String>
|
||||||
|
<system:String x:Key="close">Schließen</system:String>
|
||||||
|
<system:String x:Key="overwrite">Überschreiben</system:String>
|
||||||
|
<system:String x:Key="keepMine">Meine behalten</system:String>
|
||||||
|
<system:String x:Key="shareOnModIo">Auf mod.io teilen</system:String>
|
||||||
|
<system:String x:Key="updateOnModIo">Auf mod.io aktualisieren</system:String>
|
||||||
|
<system:String x:Key="sharedOnModIo">Auf mod.io ansehen</system:String>
|
||||||
|
<system:String x:Key="shareCopyOnModIo">Als neu auf mod.io teilen</system:String>
|
||||||
|
<system:String x:Key="enterModIoSummary">Kurze Zusammenfassung für mod.io eingeben:</system:String>
|
||||||
|
<system:String x:Key="enterModIoChangelog">Was hat sich geändert? (optional)</system:String>
|
||||||
|
<system:String x:Key="modIoShareLayoutWarning">Jeder auf mod.io kann dieses Layout herunterladen. Es enthält die aktivierten Widgets, ihre Positionen, die verwendeten Themes und ihre Nicht-Text-Einstellungen. Text-Einstellungen (z. B. URLs, Orte oder Dateipfade) und gespeicherte Widget-Daten werden nicht geteilt.</system:String>
|
||||||
|
<system:String x:Key="modIoShareThemeWarning">Jeder auf mod.io kann dieses Theme herunterladen.</system:String>
|
||||||
|
<system:String x:Key="modIoLocalPluginsWarning">Diese Widgets sind lokale Plugins, die andere nicht installieren können: {0}</system:String>
|
||||||
|
<system:String x:Key="modIoShareSuccess">"{0}" wurde auf mod.io veröffentlicht.</system:String>
|
||||||
|
<system:String x:Key="modIoUpdateSuccess">Das Update von "{0}" wurde auf mod.io veröffentlicht.</system:String>
|
||||||
|
<system:String x:Key="modIoShareFailed">Teilen auf mod.io fehlgeschlagen: {0}</system:String>
|
||||||
|
<system:String x:Key="modIoSessionExpired">Ihre mod.io-Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.</system:String>
|
||||||
|
<system:String x:Key="modIoUpdateConflict">"{0}" wurde auf mod.io aktualisiert, aber Sie haben es auch lokal geändert. Ihre Änderungen mit der neuen Version überschreiben?</system:String>
|
||||||
|
<system:String x:Key="cannotShareEmptyLayout">Das leere Layout kann nicht geteilt werden!</system:String>
|
||||||
|
<system:String x:Key="layouts">Layouts</system:String>
|
||||||
|
<system:String x:Key="searchModIo">mod.io durchsuchen...</system:String>
|
||||||
|
<system:String x:Key="searchInstalled">Installierte durchsuchen...</system:String>
|
||||||
|
<system:String x:Key="nothingFound">Nichts gefunden!</system:String>
|
||||||
|
<system:String x:Key="modIoInstallFailed">Installation von "{0}" fehlgeschlagen: {1}</system:String>
|
||||||
|
<system:String x:Key="uploadingToModIo">Wird auf mod.io hochgeladen...</system:String>
|
||||||
<col:ArrayList x:Key="musicVisualizerOptionsComboboxItems">
|
<col:ArrayList x:Key="musicVisualizerOptionsComboboxItems">
|
||||||
<system:String>Unten</system:String>
|
<system:String>Unten</system:String>
|
||||||
<system:String>Mitte</system:String>
|
<system:String>Mitte</system:String>
|
||||||
|
|||||||
@@ -72,6 +72,30 @@
|
|||||||
<system:String x:Key="releaseInfoPublishedFormat">Published: {0}</system:String>
|
<system:String x:Key="releaseInfoPublishedFormat">Published: {0}</system:String>
|
||||||
<system:String x:Key="releaseInfoBodyLabel">Release notes:</system:String>
|
<system:String x:Key="releaseInfoBodyLabel">Release notes:</system:String>
|
||||||
<system:String x:Key="openReleasePage">Open release page</system:String>
|
<system:String x:Key="openReleasePage">Open release page</system:String>
|
||||||
|
<system:String x:Key="close">Close</system:String>
|
||||||
|
<system:String x:Key="overwrite">Overwrite</system:String>
|
||||||
|
<system:String x:Key="keepMine">Keep mine</system:String>
|
||||||
|
<system:String x:Key="shareOnModIo">Share on mod.io</system:String>
|
||||||
|
<system:String x:Key="updateOnModIo">Update on mod.io</system:String>
|
||||||
|
<system:String x:Key="sharedOnModIo">View on mod.io</system:String>
|
||||||
|
<system:String x:Key="shareCopyOnModIo">Share as new on mod.io</system:String>
|
||||||
|
<system:String x:Key="enterModIoSummary">Enter a short summary shown on mod.io:</system:String>
|
||||||
|
<system:String x:Key="enterModIoChangelog">What changed? (optional)</system:String>
|
||||||
|
<system:String x:Key="modIoShareLayoutWarning">Everyone on mod.io will be able to download this layout. It includes the enabled widgets, their positions, the used themes and their non-text settings. Text settings (e.g. URLs, locations or file paths) and saved widget data are not shared.</system:String>
|
||||||
|
<system:String x:Key="modIoShareThemeWarning">Everyone on mod.io will be able to download this theme.</system:String>
|
||||||
|
<system:String x:Key="modIoLocalPluginsWarning">These widgets are local plugins that others cannot install: {0}</system:String>
|
||||||
|
<system:String x:Key="modIoShareSuccess">"{0}" was published on mod.io.</system:String>
|
||||||
|
<system:String x:Key="modIoUpdateSuccess">The update of "{0}" was published on mod.io.</system:String>
|
||||||
|
<system:String x:Key="modIoShareFailed">Sharing on mod.io failed: {0}</system:String>
|
||||||
|
<system:String x:Key="modIoSessionExpired">Your mod.io session has expired. Please log in again.</system:String>
|
||||||
|
<system:String x:Key="modIoUpdateConflict">"{0}" was updated on mod.io, but you also changed it locally. Overwrite your changes with the new version?</system:String>
|
||||||
|
<system:String x:Key="cannotShareEmptyLayout">The empty layout cannot be shared!</system:String>
|
||||||
|
<system:String x:Key="layouts">Layouts</system:String>
|
||||||
|
<system:String x:Key="searchModIo">Search mod.io...</system:String>
|
||||||
|
<system:String x:Key="searchInstalled">Search installed...</system:String>
|
||||||
|
<system:String x:Key="nothingFound">Nothing found!</system:String>
|
||||||
|
<system:String x:Key="modIoInstallFailed">Installing "{0}" failed: {1}</system:String>
|
||||||
|
<system:String x:Key="uploadingToModIo">Uploading to mod.io...</system:String>
|
||||||
<col:ArrayList x:Key="musicVisualizerOptionsComboboxItems">
|
<col:ArrayList x:Key="musicVisualizerOptionsComboboxItems">
|
||||||
<system:String>Bottom</system:String>
|
<system:String>Bottom</system:String>
|
||||||
<system:String>Middle</system:String>
|
<system:String>Middle</system:String>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace DesktopMagic.Settings;
|
namespace DesktopMagic.Settings;
|
||||||
|
|
||||||
public class Layout(string name) : INotifyPropertyChanged
|
public class Layout(string name) : INotifyPropertyChanged, IModIoShareable
|
||||||
{
|
{
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
@@ -17,6 +17,23 @@ public class Layout(string name) : INotifyPropertyChanged
|
|||||||
private string? currentThemeName = null;
|
private string? currentThemeName = null;
|
||||||
private Dictionary<uint, PluginSettings> plugins = [];
|
private Dictionary<uint, PluginSettings> plugins = [];
|
||||||
private double screenAspectRatio = 0;
|
private double screenAspectRatio = 0;
|
||||||
|
private ModIoShareState shareState;
|
||||||
|
|
||||||
|
public ModIoLink? ModIo { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public ModIoShareState ShareState
|
||||||
|
{
|
||||||
|
get => shareState;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (shareState != value)
|
||||||
|
{
|
||||||
|
shareState = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Theme Theme
|
public Theme Theme
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DesktopMagic.Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connects a local layout or theme to the mod.io mod it was published as or downloaded from.
|
||||||
|
/// </summary>
|
||||||
|
public class ModIoLink
|
||||||
|
{
|
||||||
|
public uint ModId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The modfile (version) the local copy is based on.
|
||||||
|
/// </summary>
|
||||||
|
public uint ModfileId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hash of the shareable content at the last publish or download, used to detect local changes.
|
||||||
|
/// </summary>
|
||||||
|
public string? PublishedHash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the logged in mod.io user submitted the mod and is therefore allowed to upload new versions.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsOwner { get; set; }
|
||||||
|
|
||||||
|
public Uri? ProfileUri { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ModIoShareState
|
||||||
|
{
|
||||||
|
NotShared,
|
||||||
|
UpToDate,
|
||||||
|
Modified,
|
||||||
|
NotOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A local item (layout or theme) that can be shared on mod.io.
|
||||||
|
/// </summary>
|
||||||
|
public interface IModIoShareable
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
ModIoLink? ModIo { get; set; }
|
||||||
|
|
||||||
|
ModIoShareState ShareState { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user