mirror of
https://github.com/Jellyfin2Samsung/Samsung-Jellyfin-Installer.git
synced 2026-03-01 11:21:12 +03:00
removed the need for Tizen Software and moved out of AppData cause no need anymore
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Jellyfin2SamsungCrossOS.App"
|
||||
xmlns:local="using:Jellyfin2SamsungCrossOS"
|
||||
x:Class="Jellyfin2Samsung.App"
|
||||
xmlns:local="using:Jellyfin2Samsung"
|
||||
RequestedThemeVariant="Default">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<Style Selector="Window">
|
||||
<Setter Property="Icon" Value="avares://Jellyfin2SamsungCrossOS/Assets/jelly2sams.ico" />
|
||||
<Setter Property="Icon" Value="avares://Jellyfin2Samsung/Assets/jelly2sams.ico" />
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -3,19 +3,19 @@ using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Data.Core.Plugins;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Styling;
|
||||
using Jellyfin2SamsungCrossOS.Extensions;
|
||||
using Jellyfin2SamsungCrossOS.Helpers;
|
||||
using Jellyfin2SamsungCrossOS.Services;
|
||||
using Jellyfin2SamsungCrossOS.ViewModels;
|
||||
using Jellyfin2SamsungCrossOS.Views;
|
||||
using Jellyfin2Samsung.Extensions;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Services;
|
||||
using Jellyfin2Samsung.ViewModels;
|
||||
using Jellyfin2Samsung.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS
|
||||
namespace Jellyfin2Samsung
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
@@ -29,7 +29,7 @@ namespace Jellyfin2SamsungCrossOS
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public async override void OnFrameworkInitializationCompleted()
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
ConfigureServices();
|
||||
|
||||
@@ -37,88 +37,28 @@ namespace Jellyfin2SamsungCrossOS
|
||||
{
|
||||
DisableAvaloniaDataAnnotationValidation();
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
await RequestInitialPrivilegesWithUI();
|
||||
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||
desktop.MainWindow = mainWindow;
|
||||
mainWindow.Show();
|
||||
});
|
||||
}
|
||||
else
|
||||
// Always use Dispatcher.Post for cross-platform safety
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||
desktop.MainWindow = mainWindow;
|
||||
}
|
||||
|
||||
mainWindow.Show();
|
||||
});
|
||||
}
|
||||
|
||||
RequestedThemeVariant = ThemeVariant.Light;
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
private async Task RequestInitialPrivilegesWithUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
var dialogService = _serviceProvider.GetRequiredService<IDialogService>();
|
||||
|
||||
await dialogService.ShowMessageAsync(
|
||||
"Administrator Privileges Required",
|
||||
"This application needs administrator privileges to install software on your system. " +
|
||||
"You will be prompted for your password once at the beginning.");
|
||||
|
||||
// Request privileges
|
||||
var processHelper = _serviceProvider.GetRequiredService<ProcessHelper>();
|
||||
await RequestInitialPrivileges(processHelper);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle any errors silently or log them
|
||||
Console.WriteLine($"Privilege request failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
private async Task<bool> RequestInitialPrivileges(ProcessHelper processHelper)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
// Use pkexec to authenticate, then extend sudo timeout
|
||||
var result1 = await processHelper.RunCommandAsync("pkexec", "sudo -v");
|
||||
if (result1.ExitCode == 0)
|
||||
{
|
||||
// Extend the sudo timeout to maximum (usually 15 minutes)
|
||||
var result2 = await processHelper.RunCommandAsync("sudo", "-v");
|
||||
return result2.ExitCode == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
// Test macOS authentication
|
||||
var result = await processHelper.RunCommandAsync("osascript",
|
||||
"-e \"do shell script \\\"true\\\" with administrator privileges\"");
|
||||
return result.ExitCode == 0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Authentication error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
private void ConfigureServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
var settings = AppSettings.Load();
|
||||
Debug.WriteLine($"LANG SETUP: {settings.Language}");
|
||||
|
||||
// Services
|
||||
services.AddSingleton<AppSettings>(settings);
|
||||
services.AddSingleton(settings);
|
||||
services.AddSingleton<IDialogService, DialogService>();
|
||||
services.AddSingleton<ILocalizationService, LocalizationService>();
|
||||
services.AddSingleton<INetworkService, NetworkService>();
|
||||
@@ -132,10 +72,8 @@ namespace Jellyfin2SamsungCrossOS
|
||||
services.AddSingleton<JellyfinHelper>();
|
||||
services.AddSingleton<CertificateHelper>();
|
||||
services.AddSingleton<FileHelper>();
|
||||
services.AddSingleton<OperatingSystemHelper>();
|
||||
services.AddSingleton<ProcessHelper>();
|
||||
|
||||
|
||||
// ViewModels
|
||||
services.AddSingleton<MainWindowViewModel>();
|
||||
services.AddSingleton<SettingsViewModel>();
|
||||
@@ -143,7 +81,7 @@ namespace Jellyfin2SamsungCrossOS
|
||||
services.AddTransient<InstallingWindowViewModel>();
|
||||
|
||||
// JellyfinConfigViewModel requires JellyfinHelper
|
||||
services.AddTransient<JellyfinConfigViewModel>(provider =>
|
||||
services.AddTransient(provider =>
|
||||
{
|
||||
var helper = provider.GetRequiredService<JellyfinHelper>();
|
||||
var localization = provider.GetRequiredService<ILocalizationService>();
|
||||
@@ -151,7 +89,7 @@ namespace Jellyfin2SamsungCrossOS
|
||||
});
|
||||
|
||||
// Views
|
||||
services.AddSingleton<MainWindow>(provider =>
|
||||
services.AddSingleton(provider =>
|
||||
{
|
||||
return new MainWindow
|
||||
{
|
||||
@@ -159,13 +97,13 @@ namespace Jellyfin2SamsungCrossOS
|
||||
};
|
||||
});
|
||||
|
||||
services.AddTransient<JellyfinConfigView>(provider =>
|
||||
services.AddTransient(provider =>
|
||||
{
|
||||
var vm = provider.GetRequiredService<JellyfinConfigViewModel>();
|
||||
return new JellyfinConfigView(vm);
|
||||
});
|
||||
|
||||
services.AddTransient<InstallingWindow>(provider =>
|
||||
services.AddTransient(provider =>
|
||||
{
|
||||
var vm = provider.GetRequiredService<InstallingWindowViewModel>();
|
||||
return new InstallingWindow
|
||||
@@ -174,7 +112,7 @@ namespace Jellyfin2SamsungCrossOS
|
||||
};
|
||||
});
|
||||
|
||||
services.AddTransient<InstallationCompleteWindow>(provider =>
|
||||
services.AddTransient(provider =>
|
||||
{
|
||||
var vm = provider.GetRequiredService<InstallationCompleteViewModel>();
|
||||
return new InstallationCompleteWindow(vm);
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"InstallationSuccessfulOn": "Jellyfin er blevet installeret!",
|
||||
"DownloadFailed": "Download mislykkedes:",
|
||||
"FailedLoadingReleases": "Kunne ikke indlæse udgivelser:",
|
||||
"PleaseInstallTizen": "Tizen CLI er påkrævet, men ikke fundet. Installer venligst Tizen Studio først.",
|
||||
"InstallTizenSdb": "Tizen SDB er påkrævet, men ikke fundet. Prøv at downloade igen.",
|
||||
"FailedTizenSdb": "Tizen SDB er påkrævet, men kunne ikke findes og downloades.",
|
||||
"DownloadingPackage": "Downloader pakke...",
|
||||
"ConnectingToDevice": "Opretter forbindelse til enhed...",
|
||||
"RetrievingDeviceAddress": "Henter enhedsadresse...",
|
||||
@@ -33,9 +34,7 @@
|
||||
"PostAuthorCSR": "Sender til Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Sender til Samsung distributor...",
|
||||
"CreateNewCertificates": "Opretter de signerede P12-filer...",
|
||||
"ExtractRootCertificate": "Udpakker rodcertifikater...",
|
||||
"ExportPfxCertificates": "Eksporterer PFX-certifikater...",
|
||||
"MovingP12Files": "Kopierer filer til brug i certificate-manager...",
|
||||
"SettingCertificateManager": "Retter Tizen certificate manager...",
|
||||
"SettingsCaCerts": "Indstiller CA-certifikater...",
|
||||
"ChooseRelease": "Vælg udgivelse...",
|
||||
@@ -65,8 +64,7 @@
|
||||
"UsingCustomWGT": "Bruger brugerdefineret WGT-fil",
|
||||
"FailedRemoveOld": "Kunne ikke fjerne gammel app-version",
|
||||
"FailedRemoveOldExtra": "Fjern venligst den gamle Jellyfin-app manuelt fra TV'et",
|
||||
"CheckingTizenCli": "Checker Tizen CLI...",
|
||||
"TizenCliFailed": "Tizen CLI-installation mislykkedes...",
|
||||
"CheckingTizenSdb": "Checker Tizen SDB...",
|
||||
"InitializationFailed": "Initialisering mislykkedes...",
|
||||
"DownloadingSetupFile": "Downloader installer...",
|
||||
"InstallingSetupFile": "Installerer Tizen Studio CLI...",
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"InstallationSuccessfulOn": "Jellyfin has been successfully installed!",
|
||||
"DownloadFailed": "Download failed:",
|
||||
"FailedLoadingReleases": "Failed to load releases:",
|
||||
"PleaseInstallTizen": "Tizen CLI is required but not found. Please install Tizen Studio first.",
|
||||
"InstallTizenSdb": "The Tizen SDB is required but not found. Retrying to download.",
|
||||
"FailedTizenSdb": "Tizen SDB is required but could not be found and downloaded.",
|
||||
"DownloadingPackage": "Downloading package...",
|
||||
"ConnectingToDevice": "Connecting to device...",
|
||||
"RetrievingDeviceAddress": "Retrieving device address...",
|
||||
@@ -33,9 +34,7 @@
|
||||
"PostAuthorCSR": "Posting to Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posting to Samsung distributor...",
|
||||
"CreateNewCertificates": "Creating the signed P12 files...",
|
||||
"ExtractRootCertificate": "Extracting Root Certificates...",
|
||||
"ExportPfxCertificates": "Exporting PFX certificates...",
|
||||
"MovingP12Files": "Copy files to be used in certificate-manager...",
|
||||
"SettingCertificateManager": "Fixing Tizen certificate manager...",
|
||||
"SettingsCaCerts": "Setting CA certificates...",
|
||||
"ChooseRelease": "Choose release...",
|
||||
@@ -65,8 +64,7 @@
|
||||
"UsingCustomWGT": "Using custom WGT file",
|
||||
"FailedRemoveOld": "Failed to remove old app version",
|
||||
"FailedRemoveOldExtra": "Please remove the old Jellyfin app by hand from the TV",
|
||||
"CheckingTizenCli": "Checking Tizen CLI...",
|
||||
"TizenCliFailed": "Tizen CLI installation failed...",
|
||||
"CheckingTizenSdb": "Checking Tizen SDB...",
|
||||
"InitializationFailed": "Initialization failed...",
|
||||
"DownloadingSetupFile": "Downloading installer...",
|
||||
"InstallingSetupFile": "Installing Tizen Studio CLI...",
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"InstallationSuccessfulOn": "Jellyfin is succesvol geïnstalleerd!",
|
||||
"DownloadFailed": "Download mislukt:",
|
||||
"FailedLoadingReleases": "Laden van releases mislukt:",
|
||||
"PleaseInstallTizen": "Tizen CLI is vereist maar niet gevonden. Installeer eerst Tizen Studio.",
|
||||
"InstallTizenSdb": "Tizen SDB is vereist maar niet gevonden. opnieuw proberen te downloaden.",
|
||||
"FailedTizenSdb": "Tizen SDB is vereist maar kon niet gevonden en gedownload worden.",
|
||||
"DownloadingPackage": "Pakket downloaden...",
|
||||
"ConnectingToDevice": "Verbinden met apparaat...",
|
||||
"RetrievingDeviceAddress": "Apparaatadres ophalen...",
|
||||
@@ -33,9 +34,7 @@
|
||||
"PostAuthorCSR": "Posten naar Samsung author endpoint...",
|
||||
"PostDistributorCSR": "Posten naar Samsung distributor...",
|
||||
"CreateNewCertificates": "Ondertekende P12-bestanden aanmaken...",
|
||||
"ExtractRootCertificate": "Rootcertificaten extraheren...",
|
||||
"ExportPfxCertificates": "PFX-certificaten exporteren...",
|
||||
"MovingP12Files": "Bestanden kopiëren voor gebruik in certificate-manager...",
|
||||
"SettingCertificateManager": "Tizen certificate manager repareren...",
|
||||
"SettingsCaCerts": "CA-certificaten instellen...",
|
||||
"ChooseRelease": "Release kiezen...",
|
||||
@@ -65,8 +64,7 @@
|
||||
"UsingCustomWGT": "Aangepast WGT-bestand gebruiken",
|
||||
"FailedRemoveOld": "Verwijderen oude app-versie mislukt",
|
||||
"FailedRemoveOldExtra": "Verwijder de oude Jellyfin-app handmatig van de TV",
|
||||
"CheckingTizenCli": "Tizen CLI controleren...",
|
||||
"TizenCliFailed": "Tizen CLI-installatie mislukt...",
|
||||
"CheckingTizenSdb": "Tizen SDB controleren...",
|
||||
"InitializationFailed": "Initialisatie mislukt...",
|
||||
"DownloadingSetupFile": "Installer downloaden...",
|
||||
"InstallingSetupFile": "Tizen Studio CLI installeren...",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
Do not delete me!
|
||||
|
||||
This folder will be used to place the root certificates comming from the .jar files!
|
||||
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<profiles active="dev" version="3.1">
|
||||
<profile name="dev">
|
||||
<profileitem ca="" distributor="0" key="/home/developer/author.p12" password="eF0GFnArm/35qusNw7gjmQ==" rootca=""/>
|
||||
<profileitem ca="" distributor="1" key="/home/developer/tizen-studio/tools/certificate-generator/certificates/distributor/tizen-distributor-signer.p12" password="Vy63flx5JBMc5GA4iEf8oFy+8aKE7FX/+arrDcO4I5k=" rootca=""/>
|
||||
<profileitem ca="" distributor="2" key="" password="xmEcrXPl1ss=" rootca=""/>
|
||||
</profile>
|
||||
</profiles>
|
||||
@@ -1,6 +1,6 @@
|
||||
using Jellyfin2SamsungCrossOS.Services;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Extensions
|
||||
namespace Jellyfin2Samsung.Extensions
|
||||
{
|
||||
public static class LocalizationExtensions
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Jellyfin2SamsungCrossOS.Extensions
|
||||
namespace Jellyfin2Samsung.Extensions
|
||||
{
|
||||
public delegate void ProgressCallback(string message);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class AppSettings
|
||||
{
|
||||
private const string FileName = "settings.json";
|
||||
private static readonly string FilePath =
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SamsungJellyfinInstaller", FileName);
|
||||
public static readonly string FolderPath = Environment.CurrentDirectory;
|
||||
public static readonly string FilePath = Path.Combine(FolderPath, FileName);
|
||||
public static readonly string TizenSdbPath = Path.Combine(FolderPath, "Assets", "TizenSDB");
|
||||
public static readonly string CertificatePath = Path.Combine(FolderPath, "Assets", "Certificate");
|
||||
public static readonly string ProfilePath = Path.Combine(FolderPath, "Assets", "TizenProfile");
|
||||
public static readonly string DownloadPath = Path.Combine(FolderPath, "Downloads");
|
||||
|
||||
private static AppSettings? _instance;
|
||||
|
||||
@@ -59,10 +63,8 @@ namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
// ----- Application-scoped settings (readonly at runtime) -----
|
||||
public string ReleasesUrl { get; set; } = "https://api.github.com/repos/jeppevinkel/jellyfin-tizen-builds/releases";
|
||||
public string AuthorEndpoint { get; set; } = "https://dev.tizen.samsung.com/apis/v2/authors";
|
||||
public string AppVersion { get; set; } = "v1.8.3.5";
|
||||
public string TizenCliWindows { get; set; } = "https://download.tizen.org/sdk/Installer/tizen-studio_6.1/web-cli_Tizen_Studio_6.1_windows-64.exe";
|
||||
public string TizenCliLinux { get; set; } = "https://download.tizen.org/sdk/Installer/tizen-studio_6.1/web-cli_Tizen_Studio_6.1_ubuntu-64.bin";
|
||||
public string TizenCliMac { get; set; } = "https://download.tizen.org/sdk/Installer/tizen-studio_6.1/web-cli_Tizen_Studio_6.1_macos-64.bin";
|
||||
public string AppVersion { get; set; } = "v1.8.3.6";
|
||||
public string TizenSdb { get; set; } = "https://api.github.com/repos/PatrickSt1991/tizen-sdb/releases";
|
||||
public string JellyfinAvRelease { get; set; } = "https://api.github.com/repos/PatrickSt1991/Samsung-Jellyfin-Installer/releases/239769070";
|
||||
|
||||
public AppSettings() { }
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class CertificateHelper
|
||||
{
|
||||
public List<ExistingCertificates> GetAvailableCertificates(string profilePath, string tizenCrypto)
|
||||
public List<ExistingCertificates> GetAvailableCertificates(string certificateFolders)
|
||||
{
|
||||
var certificates = new List<ExistingCertificates>();
|
||||
var cipherUtil = new CipherUtil();
|
||||
List<string> duids = new List<string>();
|
||||
|
||||
// Default item
|
||||
certificates.Add(new ExistingCertificates
|
||||
@@ -29,122 +27,53 @@ namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
ExpireDate = null
|
||||
});
|
||||
|
||||
if (!File.Exists(profilePath))
|
||||
if (!Directory.Exists(certificateFolders))
|
||||
return certificates;
|
||||
|
||||
try
|
||||
{
|
||||
var doc = XDocument.Load(profilePath);
|
||||
var profiles = doc.Root?.Elements("profile");
|
||||
|
||||
var p12Files = Directory.GetFiles(
|
||||
certificateFolders,
|
||||
"author.p12",
|
||||
SearchOption.AllDirectories);
|
||||
|
||||
if (profiles == null)
|
||||
return certificates;
|
||||
|
||||
foreach (var profile in profiles)
|
||||
foreach(var p12Path in p12Files)
|
||||
{
|
||||
string? name = profile.Attribute("name")?.Value;
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
var directory = Path.GetDirectoryName(p12Path);
|
||||
if (directory == null)
|
||||
continue;
|
||||
|
||||
// Author Certificate
|
||||
var authorItem = profile.Elements("profileitem")
|
||||
.FirstOrDefault(p => p.Attribute("distributor")?.Value == "0");
|
||||
|
||||
string? keyPath = authorItem?.Attribute("key")?.Value;
|
||||
string? encryptedPassword = authorItem?.Attribute("password")?.Value;
|
||||
DateTime? expireDate = null;
|
||||
string? decryptedPassword = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(keyPath) && File.Exists(keyPath) && !string.IsNullOrEmpty(encryptedPassword))
|
||||
{
|
||||
if (File.Exists(encryptedPassword))
|
||||
decryptedPassword = cipherUtil.RunWincryptDecrypt(encryptedPassword, tizenCrypto);
|
||||
else if (IsBase64String(encryptedPassword))
|
||||
decryptedPassword = cipherUtil.GetDecryptedString(encryptedPassword);
|
||||
else
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
var cert = new X509Certificate2(keyPath, decryptedPassword, X509KeyStorageFlags.Exportable);
|
||||
expireDate = cert.NotAfter;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to read certificate '{keyPath}': {ex.Message}");
|
||||
}
|
||||
|
||||
if (expireDate.HasValue && expireDate.Value.Date >= DateTime.Today)
|
||||
{
|
||||
// Retrieve distributor certificate for DUID
|
||||
string duid = ExtractDistributorDuid(profile, cipherUtil, tizenCrypto);
|
||||
|
||||
certificates.Add(new ExistingCertificates
|
||||
{
|
||||
Name = name,
|
||||
File = keyPath,
|
||||
ExpireDate = expireDate,
|
||||
Duid = duid
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error reading profile XML: {ex.Message}");
|
||||
}
|
||||
|
||||
return certificates;
|
||||
}
|
||||
private string ExtractDistributorDuid(XElement profile, CipherUtil cipherUtil, string tizenCrypto)
|
||||
{
|
||||
List<string> duids = new List<string>();
|
||||
|
||||
var distributorItem = profile.Elements("profileitem")
|
||||
.FirstOrDefault(p => p.Attribute("distributor")?.Value == "1");
|
||||
|
||||
if (distributorItem == null)
|
||||
return string.Empty;
|
||||
|
||||
string? keyPath = distributorItem.Attribute("key")?.Value;
|
||||
string? encryptedPassword = distributorItem.Attribute("password")?.Value;
|
||||
string? decryptedPassword = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(keyPath) && File.Exists(keyPath) && !string.IsNullOrEmpty(encryptedPassword))
|
||||
{
|
||||
if (File.Exists(encryptedPassword))
|
||||
decryptedPassword = cipherUtil.RunWincryptDecrypt(encryptedPassword, tizenCrypto);
|
||||
else if (IsBase64String(encryptedPassword))
|
||||
decryptedPassword = cipherUtil.GetDecryptedString(encryptedPassword);
|
||||
var passwordPath = Path.Combine(directory, "password.txt");
|
||||
if (!File.Exists(passwordPath))
|
||||
continue;
|
||||
|
||||
var password = File.ReadAllText(passwordPath).Trim();
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
continue;
|
||||
try
|
||||
{
|
||||
var distributorCert = new X509Certificate2(keyPath, decryptedPassword, X509KeyStorageFlags.Exportable);
|
||||
foreach (var ext in distributorCert.Extensions)
|
||||
var cert = new X509Certificate2(
|
||||
p12Path,
|
||||
password,
|
||||
X509KeyStorageFlags.Exportable);
|
||||
|
||||
if(cert.NotAfter.Date >= DateTime.Today)
|
||||
{
|
||||
var raw = ext.Format(true);
|
||||
foreach (Match match in Regex.Matches(raw, @"URN:tizen:deviceid=([A-Za-z0-9]+)"))
|
||||
certificates.Add(new ExistingCertificates
|
||||
{
|
||||
string duid = match.Groups[1].Value;
|
||||
if (!duids.Contains(duid))
|
||||
duids.Add(duid);
|
||||
}
|
||||
Name = cert.GetNameInfo(X509NameType.SimpleName, forIssuer: false),
|
||||
File = p12Path,
|
||||
ExpireDate = cert.NotAfter,
|
||||
Duid = string.Empty
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to read distributor certificate '{keyPath}': {ex.Message}");
|
||||
Debug.WriteLine($"Failed to load certificate '{p12Path}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join(",", duids); // ✅ Return comma-separated list
|
||||
}
|
||||
public static bool IsBase64String(string s)
|
||||
{
|
||||
s = s.Trim();
|
||||
return (s.Length % 4 == 0) &&
|
||||
Regex.IsMatch(s, @"^[A-Za-z0-9\+/]*={0,2}$");
|
||||
return certificates;
|
||||
}
|
||||
public async Task HandleErrorResponse(HttpResponseMessage response)
|
||||
{
|
||||
|
||||
@@ -1,92 +1,15 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class CipherUtil
|
||||
{
|
||||
private const string FallbackKeyString = "KYANINYLhijklmnopqrstuvwx";
|
||||
private string _usedPassword = FallbackKeyString;
|
||||
private const string KeyString = "KYANINYLhijklmnopqrstuvwx";
|
||||
|
||||
private byte[] KeyBytes => Encoding.UTF8.GetBytes(_usedPassword).Take(24).ToArray();
|
||||
|
||||
public async Task<string> ExtractPasswordAsync(string jarPath)
|
||||
{
|
||||
string? extracted = await TryExtractFromJarAsync(jarPath);
|
||||
if (!string.IsNullOrEmpty(extracted))
|
||||
{
|
||||
_usedPassword = extracted;
|
||||
return extracted;
|
||||
}
|
||||
|
||||
_usedPassword = FallbackKeyString;
|
||||
return FallbackKeyString;
|
||||
}
|
||||
|
||||
private async Task<string?> TryExtractFromJarAsync(string jarPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var jarFiles = Directory.GetFiles(jarPath, "*.jar");
|
||||
foreach (var jar in jarFiles)
|
||||
{
|
||||
string fileName = Path.GetFileName(jar);
|
||||
if (!fileName.StartsWith("org.tizen.common.cert") || !fileName.EndsWith(".jar"))
|
||||
continue;
|
||||
|
||||
using var fs = File.OpenRead(jar);
|
||||
using var ms = new MemoryStream();
|
||||
await fs.CopyToAsync(ms);
|
||||
ms.Position = 0;
|
||||
|
||||
using var zip = new ZipArchive(ms, ZipArchiveMode.Read);
|
||||
foreach (var entry in zip.Entries)
|
||||
{
|
||||
if (!entry.FullName.EndsWith("CipherUtil.class", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
using var classStream = entry.Open();
|
||||
var password = ExtractPasswordFromClassSimple(classStream);
|
||||
if (!string.IsNullOrEmpty(password))
|
||||
return password;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Cipher extraction failed: {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? ExtractPasswordFromClassSimple(Stream classStream)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var reader = new StreamReader(classStream);
|
||||
string content = reader.ReadToEnd();
|
||||
|
||||
string knownPassword = FallbackKeyString;
|
||||
int index = content.IndexOf(knownPassword, StringComparison.Ordinal);
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
string extracted = content.Substring(index, knownPassword.Length);
|
||||
if (extracted.All(char.IsLetterOrDigit) && extracted.Length == 26)
|
||||
return extracted;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return null;
|
||||
}
|
||||
private byte[] KeyBytes => Encoding.UTF8.GetBytes(KeyString).Take(24).ToArray();
|
||||
|
||||
public string GetEncryptedString(string plainText)
|
||||
{
|
||||
@@ -101,8 +24,6 @@ namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
byte[] encrypted = tdes.CreateEncryptor().TransformFinalBlock(data, 0, data.Length);
|
||||
return Convert.ToBase64String(encrypted);
|
||||
}
|
||||
|
||||
|
||||
public string GetDecryptedString(string encryptedBase64)
|
||||
{
|
||||
byte[] encryptedBytes = Convert.FromBase64String(encryptedBase64);
|
||||
@@ -115,7 +36,6 @@ namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
byte[] decrypted = tripleDes.CreateDecryptor().TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length);
|
||||
return Encoding.UTF8.GetString(decrypted);
|
||||
}
|
||||
|
||||
public string GenerateRandomPassword(int length = 12)
|
||||
{
|
||||
if (length < 8)
|
||||
@@ -140,22 +60,5 @@ namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
|
||||
return new string(chars.OrderBy(_ => Guid.NewGuid()).ToArray());
|
||||
}
|
||||
|
||||
public string RunWincryptDecrypt(string filePath, string cryptoPath)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = cryptoPath,
|
||||
Arguments = $"--decrypt \"{filePath}\"",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = Process.Start(psi)!;
|
||||
string output = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
return output.Split(new[] { "PASSWORD:" }, StringSplitOptions.None)[1].Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2SamsungCrossOS.Services;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
@@ -10,7 +10,7 @@ using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class DeviceHelper
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Avalonia.Platform.Storage;
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -7,7 +7,7 @@ using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class FileHelper
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2SamsungCrossOS.Services;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -12,7 +11,7 @@ using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class JellyfinHelper
|
||||
{
|
||||
@@ -113,10 +112,8 @@ namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
|
||||
return users;
|
||||
}
|
||||
public async Task<InstallResult> ApplyConfigAndResignPackageAsync(
|
||||
string TizenCliPath,
|
||||
public async Task<InstallResult> ApplyJellyfinConfigAsync(
|
||||
string packagePath,
|
||||
string certificateName,
|
||||
string[] userIds)
|
||||
{
|
||||
string? tempDir = null;
|
||||
@@ -152,10 +149,6 @@ namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
File.Delete(packagePath);
|
||||
File.Move(tempPackage, packagePath);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
await _processHelper.RunCommandCmdAsync(TizenCliPath, $"sign --signing-profile {certificateName} \"{packagePath}\"");
|
||||
else
|
||||
await _processHelper.RunCommandAsync(TizenCliPath, $"sign --signing-profile {certificateName} \"{packagePath}\"");
|
||||
|
||||
return InstallResult.SuccessResult();
|
||||
}
|
||||
@@ -292,11 +285,11 @@ namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
// Update additional user configurations
|
||||
var userConfig = new
|
||||
{
|
||||
PlayDefaultAudioTrack = AppSettings.Default.PlayDefaultAudioTrack,
|
||||
SubtitleLanguagePreference = AppSettings.Default.SubtitleLanguagePreference,
|
||||
AppSettings.Default.PlayDefaultAudioTrack,
|
||||
AppSettings.Default.SubtitleLanguagePreference,
|
||||
SubtitleMode = AppSettings.Default.SelectedSubtitleMode,
|
||||
RememberAudioSelections = AppSettings.Default.RememberAudioSelections,
|
||||
RememberSubtitleSelections = AppSettings.Default.RememberSubtitleSelections,
|
||||
AppSettings.Default.RememberAudioSelections,
|
||||
AppSettings.Default.RememberSubtitleSelections,
|
||||
EnableNextEpisodeAutoPlay = AppSettings.Default.AutoPlayNextEpisode,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
{
|
||||
public class OperatingSystemHelper
|
||||
{
|
||||
public string GetInstallPath()
|
||||
{
|
||||
string baseDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return Path.Combine(baseDir, "Programs", "TizenStudioCli");
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "TizenStudioCli");
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Applications", "TizenStudioCli");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PlatformNotSupportedException("Unsupported OS");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
using Jellyfin2SamsungCrossOS.Extensions;
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2SamsungCrossOS.Services;
|
||||
using Jellyfin2Samsung;
|
||||
using Jellyfin2Samsung.Extensions;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Jellyfin2Samsung;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class PackageHelper
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
@@ -7,7 +7,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Helpers
|
||||
namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
public class ProcessHelper
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>Jellyfin2SamsungCrossOS</string>
|
||||
<string>Jellyfin2Samsung</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Jellyfin2Samsung</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Jellyfin2SamsungCrossOS</string>
|
||||
<string>Jellyfin2Samsung</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>jelly2sams.icns</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Services
|
||||
namespace Jellyfin2Samsung.Interfaces
|
||||
{
|
||||
public interface IDialogService
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Services
|
||||
namespace Jellyfin2Samsung.Interfaces
|
||||
{
|
||||
public interface ILocalizationService
|
||||
{
|
||||
@@ -1,10 +1,10 @@
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Services
|
||||
namespace Jellyfin2Samsung.Interfaces
|
||||
{
|
||||
public interface INetworkService
|
||||
{
|
||||
@@ -0,0 +1,10 @@
|
||||
using Jellyfin2Samsung.Extensions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Interfaces
|
||||
{
|
||||
public interface ITizenCertificateService
|
||||
{
|
||||
Task<(string authorP12, string distributorP12, string passwordP12)> GenerateProfileAsync(string duid, string accessToken, string userId, string userEmail, string outputPath, ProgressCallback? progress = null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Jellyfin2Samsung.Extensions;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Interfaces
|
||||
{
|
||||
public interface ITizenInstallerService
|
||||
{
|
||||
Task<string> GetTvNameAsync(string tvIpAddress);
|
||||
Task<string> EnsureTizenSdbAvailable();
|
||||
Task<string> DownloadPackageAsync(string downloadUrl);
|
||||
Task<InstallResult> InstallPackageAsync(string packageUrl, string tvIpAddress, ProgressCallback? progress = null);
|
||||
}
|
||||
}
|
||||
@@ -6,23 +6,24 @@
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
<AssemblyName>Jellyfin2SamsungCrossOS</AssemblyName>
|
||||
<AssemblyName>Jellyfin2Samsung</AssemblyName>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<!-- Windows icon -->
|
||||
<ApplicationIcon>Assets\jelly2sams.ico</ApplicationIcon>
|
||||
<!-- macOS bundle info -->
|
||||
<CFBundleName>Jellyfin2SamsungCrossOS</CFBundleName>
|
||||
<CFBundleName>Jellyfin2Samsung</CFBundleName>
|
||||
<CFBundleDisplayName>Jellyfin to Samsung TV</CFBundleDisplayName>
|
||||
<CFBundleIdentifier>com.yourcompany.jellyfin2samsung</CFBundleIdentifier>
|
||||
<CFBundleVersion>1.0.0</CFBundleVersion>
|
||||
<CFBundlePackageType>APPL</CFBundlePackageType>
|
||||
<CFBundleSignature>????</CFBundleSignature>
|
||||
<CFBundleExecutable>Jellyfin2SamsungCrossOS</CFBundleExecutable>
|
||||
<CFBundleExecutable>Jellyfin2Samsung</CFBundleExecutable>
|
||||
<CFBundleIconFile>jelly2sams.icns</CFBundleIconFile>
|
||||
<!-- Linux desktop entry info -->
|
||||
<LinuxDesktopFile>true</LinuxDesktopFile>
|
||||
<LinuxDesktopFileName>jellyfin2samsung.desktop</LinuxDesktopFileName>
|
||||
<PackageId>Jellyfin2Samsung</PackageId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -77,4 +78,8 @@
|
||||
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
|
||||
<PackageReference Include="System.Formats.Asn1" Version="6.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Assets\TizenSDB\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Jellyfin2SamsungCrossOS.Models
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
internal class DeviceCapabilities
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Models
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
public class ExistingCertificates
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Models
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
|
||||
public class GitHubRelease
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Jellyfin2SamsungCrossOS.Models
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
public class InstallResult
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Jellyfin2SamsungCrossOS.Models
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
public class JellyfinAuth
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Jellyfin2SamsungCrossOS.Models
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
public class LanguageOption
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Jellyfin2SamsungCrossOS.Models
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
public class NetworkDevice
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Jellyfin2SamsungCrossOS.Models
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
public class ProcessResult
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Jellyfin2SamsungCrossOS.Models
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
public class SamsungAuth
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS
|
||||
namespace Jellyfin2Samsung
|
||||
{
|
||||
internal sealed class Program
|
||||
{
|
||||
|
||||
@@ -4,14 +4,16 @@ using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Services
|
||||
namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
public class DialogService : IDialogService
|
||||
{
|
||||
private Window? GetMainWindow()
|
||||
{
|
||||
if (Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop)
|
||||
if (Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop)
|
||||
return desktop.MainWindow;
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
using Jellyfin2SamsungCrossOS.Extensions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Services
|
||||
{
|
||||
public interface ITizenCertificateService
|
||||
{
|
||||
Task<(string p12Location, string p12Password)> GenerateProfileAsync(string duid, string accessToken, string userId, string userEmail, string outputPath, string jarPath, ProgressCallback? progress = null);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using Jellyfin2SamsungCrossOS.Extensions;
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Services
|
||||
{
|
||||
public interface ITizenInstallerService
|
||||
{
|
||||
string TizenCliPath { get; }
|
||||
Task<(string, string)> EnsureTizenCliAvailable();
|
||||
Task<string> DownloadPackageAsync(string downloadUrl);
|
||||
Task<InstallResult> InstallPackageAsync(string packageUrl, string tvIpAddress, ProgressCallback? progress = null);
|
||||
Task<string?> GetTvNameAsync(string tvIpAddress);
|
||||
Task<bool> ConnectToTvAsync(string tvIpAddress);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
using Avalonia.Platform;
|
||||
using Jellyfin2SamsungCrossOS.Helpers;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Services
|
||||
namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
public class LocalizationService : ILocalizationService
|
||||
{
|
||||
@@ -31,7 +32,7 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri($"avares://Jellyfin2SamsungCrossOS/Assets/Localization/{lang}.json");
|
||||
var uri = new Uri($"avares://Jellyfin2Samsung/Assets/Localization/{lang}.json");
|
||||
var asset = AssetLoader.Open(uri);
|
||||
|
||||
using var reader = new StreamReader(asset);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Jellyfin2SamsungCrossOS.Helpers;
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -12,7 +13,7 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Services
|
||||
namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
public class NetworkService : INetworkService
|
||||
{
|
||||
@@ -120,8 +121,8 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
.Where(ni =>
|
||||
virtualScan
|
||||
? true
|
||||
: (ni.NetworkInterfaceType == NetworkInterfaceType.Ethernet ||
|
||||
ni.NetworkInterfaceType == NetworkInterfaceType.Wireless80211))
|
||||
: ni.NetworkInterfaceType == NetworkInterfaceType.Ethernet ||
|
||||
ni.NetworkInterfaceType == NetworkInterfaceType.Wireless80211)
|
||||
.SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
|
||||
.Where(ip => ip.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||
.Where(ip => !IPAddress.IsLoopback(ip.Address))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Avalonia.Controls;
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -9,7 +9,7 @@ using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Services
|
||||
namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
public class SamsungLoginService
|
||||
{
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
using Jellyfin2SamsungCrossOS.Extensions;
|
||||
using Jellyfin2SamsungCrossOS.Helpers;
|
||||
using Jellyfin2Samsung.Extensions;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Org.BouncyCastle.Asn1;
|
||||
using Org.BouncyCastle.Asn1.Pkcs;
|
||||
using Org.BouncyCastle.Asn1.X509;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.OpenSsl;
|
||||
using Org.BouncyCastle.Pkcs;
|
||||
using Org.BouncyCastle.Security;
|
||||
using Org.BouncyCastle.X509;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Services
|
||||
namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
public class TizenCertificateService : ITizenCertificateService
|
||||
{
|
||||
@@ -34,23 +32,18 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
_dialogService = dialogService;
|
||||
}
|
||||
|
||||
public async Task<(string p12Location, string p12Password)> GenerateProfileAsync(
|
||||
public async Task<(string authorP12, string distributorP12, string passwordP12)> GenerateProfileAsync(
|
||||
string duid,
|
||||
string accessToken,
|
||||
string userId,
|
||||
string userEmail,
|
||||
string outputPath,
|
||||
string jarPath,
|
||||
ProgressCallback? progress = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(outputPath))
|
||||
throw new ArgumentException("Output path cannot be empty", nameof(outputPath));
|
||||
|
||||
if (string.IsNullOrEmpty(jarPath) || !Directory.Exists(jarPath))
|
||||
throw new ArgumentException($"Invalid jarPath: {jarPath}", nameof(jarPath));
|
||||
|
||||
var cipherUtil = new CipherUtil();
|
||||
await cipherUtil.ExtractPasswordAsync(jarPath);
|
||||
|
||||
Directory.CreateDirectory(outputPath);
|
||||
|
||||
@@ -80,34 +73,12 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
await File.WriteAllBytesAsync(Path.Combine(outputPath, "device-profile.xml"), profileXmlBytes);
|
||||
await File.WriteAllBytesAsync(Path.Combine(outputPath, "signed_distributor.cer"), signedDistributorCsrBytes);
|
||||
|
||||
progress?.Invoke("ExtractRootCertificate".Localized());
|
||||
await ExtractRootCertificateAsync(jarPath);
|
||||
|
||||
await CheckCertificateExistenceAsync(Path.Combine(outputPath, "ca"));
|
||||
await CheckCertificateExistenceAsync(Path.Combine(AppSettings.ProfilePath, "ca"));
|
||||
|
||||
progress?.Invoke("ExportPfxCertificates".Localized());
|
||||
await ExportPfxWithCaChainAsync(signedAuthorCsrBytes, keyPair.Private, p12Plain, outputPath, Path.Combine(outputPath, "ca"), "author", "vd_tizen_dev_author_ca.cer");
|
||||
await ExportPfxWithCaChainAsync(signedDistributorCsrBytes, keyPair.Private, p12Plain, outputPath, Path.Combine(outputPath, "ca"), "distributor", "vd_tizen_dev_public2.crt");
|
||||
|
||||
// in GenerateProfileAsync, right after the two exports:
|
||||
System.Diagnostics.Debug.WriteLine("[CERT] ExportPfx done, about to invoke progress: MovingP12Files");
|
||||
try
|
||||
{
|
||||
progress?.Invoke("MovingP12Files".Localized());
|
||||
System.Diagnostics.Debug.WriteLine("[CERT] progress.Invoke(MovingP12Files) returned");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CERT] progress.Invoke threw: {ex}");
|
||||
throw;
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine("[CERT] Calling MoveTizenCertificateFiles()");
|
||||
string p12Location = MoveTizenCertificateFiles();
|
||||
System.Diagnostics.Debug.WriteLine($"[CERT] MoveTizenCertificateFiles() returned: {p12Location}");
|
||||
|
||||
|
||||
return (p12Location, p12Encrypted);
|
||||
string authorp12 = await ExportPfxWithCaChainAsync(signedAuthorCsrBytes, keyPair.Private, p12Plain, outputPath, Path.Combine(AppSettings.ProfilePath, "ca"), "author", "vd_tizen_dev_author_ca.cer");
|
||||
string distributorp12 = await ExportPfxWithCaChainAsync(signedDistributorCsrBytes, keyPair.Private, p12Plain, outputPath, Path.Combine(AppSettings.ProfilePath, "ca"), "distributor", "vd_tizen_dev_public2.crt");
|
||||
return (authorp12, distributorp12, p12Plain);
|
||||
}
|
||||
|
||||
private static AsymmetricCipherKeyPair GenerateKeyPair()
|
||||
@@ -156,19 +127,12 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
private async Task CheckCertificateExistenceAsync(string caPath)
|
||||
{
|
||||
string[] requiredFiles = { "vd_tizen_dev_author_ca.cer", "vd_tizen_dev_public2.crt" };
|
||||
string caLocalPath = Path.Combine(caPath, "ca_local");
|
||||
|
||||
foreach (var file in requiredFiles)
|
||||
{
|
||||
string target = Path.Combine(caPath, file);
|
||||
if (!File.Exists(target))
|
||||
{
|
||||
string source = Path.Combine(caLocalPath, file);
|
||||
if (!File.Exists(source))
|
||||
await _dialogService.ShowErrorAsync($"Missing CA file: {file}");
|
||||
else
|
||||
File.Copy(source, target, true);
|
||||
}
|
||||
await _dialogService.ShowErrorAsync($"Missing CA file: {file}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,31 +204,8 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
return (profileXml, distributorCert);
|
||||
}
|
||||
|
||||
public async Task ExtractRootCertificateAsync(string jarPath)
|
||||
{
|
||||
if (!Directory.Exists(jarPath)) return;
|
||||
|
||||
foreach (var jar in Directory.GetFiles(jarPath, "*.jar"))
|
||||
{
|
||||
if (!Path.GetFileName(jar).StartsWith("org.tizen.common.cert")) continue;
|
||||
using var fs = File.OpenRead(jar);
|
||||
using var zip = new ZipArchive(fs, ZipArchiveMode.Read);
|
||||
foreach (var entry in zip.Entries)
|
||||
{
|
||||
string fileName = Path.GetFileName(entry.FullName);
|
||||
if (fileName == "vd_tizen_dev_author_ca.cer" || fileName == "vd_tizen_dev_public2.crt")
|
||||
{
|
||||
string target = Path.Combine("Assets", "TizenProfile", "ca", fileName);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
|
||||
using var outStream = File.Create(target);
|
||||
using var entryStream = entry.Open();
|
||||
await entryStream.CopyToAsync(outStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ExportPfxWithCaChainAsync(
|
||||
private static async Task<string> ExportPfxWithCaChainAsync(
|
||||
byte[] signedCertBytes,
|
||||
AsymmetricKeyParameter privateKey,
|
||||
string password,
|
||||
@@ -338,7 +279,7 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
var target = Path.Combine(outputPath, $"{filename}.p12");
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
store.Save(ms, password.ToCharArray(), new Org.BouncyCastle.Security.SecureRandom());
|
||||
store.Save(ms, password.ToCharArray(), new SecureRandom());
|
||||
await File.WriteAllBytesAsync(target, ms.ToArray());
|
||||
}
|
||||
|
||||
@@ -351,48 +292,8 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
?? throw new InvalidOperationException("PFX sanity failed: no intermediate certificate.");
|
||||
if (!string.Equals(leaf.Issuer, ca.Subject, StringComparison.Ordinal))
|
||||
throw new InvalidOperationException($"PFX chain mismatch: leaf issuer '{leaf.Issuer}' != CA subject '{ca.Subject}'.");
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
public static string MoveTizenCertificateFiles()
|
||||
{
|
||||
string dest = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
"SamsungCertificate", "Jelly2Sams");
|
||||
string src = Path.Combine(Environment.CurrentDirectory, "Assets", "TizenProfile");
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[CERT] Move start: src='{src}', dest='{dest}'");
|
||||
|
||||
Directory.CreateDirectory(dest);
|
||||
|
||||
if (!Directory.Exists(src))
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("[CERT] SRC does not exist!");
|
||||
return dest;
|
||||
}
|
||||
|
||||
var files = Directory.GetFiles(src, "*.*");
|
||||
System.Diagnostics.Debug.WriteLine($"[CERT] Files to move: {files.Length}");
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
var target = Path.Combine(dest, Path.GetFileName(file)!);
|
||||
var len = new FileInfo(file).Length;
|
||||
System.Diagnostics.Debug.WriteLine($"[CERT] Moving '{file}' ({len} bytes) -> '{target}'");
|
||||
File.Move(file, target, true);
|
||||
System.Diagnostics.Debug.WriteLine($"[CERT] Moved '{file}'");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CERT] Move failed for '{file}': {ex}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine("[CERT] Move complete");
|
||||
return dest;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,30 @@
|
||||
using Avalonia.Threading;
|
||||
using Jellyfin2SamsungCrossOS.Extensions;
|
||||
using Jellyfin2SamsungCrossOS.Helpers;
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2Samsung.Extensions;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Services
|
||||
namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
public class TizenInstallerService : ITizenInstallerService
|
||||
{
|
||||
private static readonly string[] PossibleTizenPaths = GetPossibleTizenPaths();
|
||||
|
||||
private static string[] GetPossibleTizenPaths()
|
||||
{
|
||||
var paths = new List<string>();
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
paths.Add(@"C:\TizenStudioCli");
|
||||
paths.Add(Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Programs",
|
||||
"TizenStudioCli"));
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
paths.Add(Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
"TizenStudioCli"));
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
paths.Add(Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
"tizen-studio-cli"));
|
||||
}
|
||||
|
||||
return paths.ToArray();
|
||||
}
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly AppSettings _appSettings;
|
||||
private readonly JellyfinHelper _jellyfinHelper;
|
||||
private readonly OperatingSystemHelper _osHelper;
|
||||
private readonly ProcessHelper _processHelper;
|
||||
private readonly FileHelper _fileHelper;
|
||||
private readonly string _downloadDirectory;
|
||||
private string _installPath;
|
||||
private const int MaxSafePathLength = 240;
|
||||
|
||||
public string? TizenRootPath { get; private set; }
|
||||
public string? TizenCliPath { get; private set; }
|
||||
public string? TizenSdbPath { get; private set; }
|
||||
public string? TizenCypto { get; private set; }
|
||||
public string? TizenPluginPath { get; private set; }
|
||||
public string? TizenDataPath { get; private set; }
|
||||
public string? PackageCertificate { get; set; }
|
||||
|
||||
public TizenInstallerService(
|
||||
@@ -70,7 +32,6 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
IDialogService dialogService,
|
||||
AppSettings appSettings,
|
||||
JellyfinHelper jellyfinHelper,
|
||||
OperatingSystemHelper osHelper,
|
||||
ProcessHelper processHelper,
|
||||
FileHelper fileHelper)
|
||||
{
|
||||
@@ -78,180 +39,141 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
_dialogService = dialogService;
|
||||
_appSettings = appSettings;
|
||||
_jellyfinHelper = jellyfinHelper;
|
||||
_osHelper = osHelper;
|
||||
_processHelper = processHelper;
|
||||
_fileHelper = fileHelper;
|
||||
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("SamsungJellyfinInstaller/1.0");
|
||||
|
||||
_downloadDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"SamsungJellyfinInstaller",
|
||||
"Downloads");
|
||||
|
||||
Directory.CreateDirectory(_downloadDirectory);
|
||||
|
||||
DetermineInstallPath();
|
||||
InitializeTizenPaths();
|
||||
}
|
||||
|
||||
private void InitializeTizenPaths()
|
||||
public async Task<string> EnsureTizenSdbAvailable()
|
||||
{
|
||||
string? tizenRoot = FindTizenRoot();
|
||||
string tizenSdbPath = AppSettings.TizenSdbPath;
|
||||
|
||||
if (tizenRoot is not null)
|
||||
// Find existing versioned file
|
||||
var existingFile = Directory.GetFiles(tizenSdbPath, GetSearchPattern())
|
||||
.FirstOrDefault();
|
||||
|
||||
// Get latest version from GitHub
|
||||
var latestVersion = await GetLatestTizenSdbVersionAsync();
|
||||
|
||||
// Check if we need to update
|
||||
if (existingFile != null && !ShouldUpdateBinary(existingFile, latestVersion))
|
||||
{
|
||||
TizenRootPath = tizenRoot;
|
||||
|
||||
// CLI launcher
|
||||
TizenCliPath = Path.Combine(
|
||||
tizenRoot, "tools", "ide", "bin",
|
||||
OperatingSystem.IsWindows() ? "tizen.bat" : "tizen"
|
||||
);
|
||||
|
||||
// SDB
|
||||
TizenSdbPath = Path.Combine(
|
||||
tizenRoot, "tools",
|
||||
OperatingSystem.IsWindows() ? "sdb.exe" : "sdb"
|
||||
);
|
||||
|
||||
// Crypto tool (Windows only)
|
||||
TizenCypto = OperatingSystem.IsWindows()
|
||||
? Path.Combine(tizenRoot, "tools", "certificate-encryptor", "wincrypt.exe")
|
||||
: null; // no wincrypt on Linux/macOS
|
||||
|
||||
// Plugins
|
||||
TizenPluginPath = Path.Combine(tizenRoot, "ide", "plugins");
|
||||
|
||||
// Data path (profiles.xml)
|
||||
string tizenDataRoot = Path.Combine(
|
||||
Path.GetDirectoryName(tizenRoot) ?? tizenRoot,
|
||||
Path.GetFileName(tizenRoot) + "-data"
|
||||
);
|
||||
TizenDataPath = Path.Combine(tizenDataRoot, "profile", "profiles.xml");
|
||||
TizenSdbPath = existingFile;
|
||||
return TizenSdbPath;
|
||||
}
|
||||
else
|
||||
|
||||
// Download new version
|
||||
string downloadedFile = await DownloadTizenSdbAsync(latestVersion);
|
||||
|
||||
// Remove old file if it exists
|
||||
if (existingFile != null && File.Exists(existingFile))
|
||||
{
|
||||
TizenRootPath = null;
|
||||
TizenCliPath = null;
|
||||
TizenSdbPath = null;
|
||||
TizenCypto = null;
|
||||
TizenPluginPath = null;
|
||||
TizenDataPath = null;
|
||||
File.Delete(existingFile);
|
||||
}
|
||||
|
||||
// Move to final location
|
||||
string finalPath = Path.Combine(tizenSdbPath, GetFinalFileName(latestVersion));
|
||||
File.Move(downloadedFile, finalPath, true);
|
||||
|
||||
TizenSdbPath = finalPath;
|
||||
return TizenSdbPath;
|
||||
}
|
||||
private void DetermineInstallPath()
|
||||
|
||||
private string GetSearchPattern()
|
||||
{
|
||||
string defaultPath;
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
defaultPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Programs",
|
||||
"TizenStudioCli"
|
||||
);
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
defaultPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
"Library", "TizenStudioCli"
|
||||
);
|
||||
}
|
||||
else // Linux
|
||||
{
|
||||
defaultPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
"tizen-studio-cli"
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback only for Windows (path length issues)
|
||||
var fallbackPath = OperatingSystem.IsWindows()
|
||||
? "C:\\TizenStudioCli"
|
||||
: defaultPath;
|
||||
|
||||
if (defaultPath.Length > MaxSafePathLength)
|
||||
{
|
||||
_dialogService.ShowMessageAsync("Path length exceeded", "Path length exceeded the safe limit. Using fallback path.").Wait();
|
||||
_installPath = fallbackPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
_installPath = defaultPath;
|
||||
}
|
||||
if (OperatingSystem.IsWindows()) return "TizenSdb*.exe";
|
||||
if (OperatingSystem.IsLinux()) return "TizenSdb*_linux";
|
||||
if (OperatingSystem.IsMacOS()) return "TizenSdb*_macos";
|
||||
throw new PlatformNotSupportedException("Unsupported OS");
|
||||
}
|
||||
public async Task<(string?, string?)> EnsureTizenCliAvailable()
|
||||
|
||||
private string GetFinalFileName(string version)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(TizenRootPath))
|
||||
{
|
||||
bool cliOk = File.Exists(TizenCliPath) && File.Exists(TizenSdbPath);
|
||||
|
||||
// crypto tool only matters on Windows
|
||||
bool cryptoOk = OperatingSystem.IsWindows()
|
||||
? File.Exists(TizenCypto)
|
||||
: true;
|
||||
|
||||
string certManagerExe = OperatingSystem.IsWindows()
|
||||
? "certificate-manager.exe"
|
||||
: "certificate-manager";
|
||||
|
||||
string[] certManagerPaths = {
|
||||
Path.Combine(TizenRootPath, "certificate-manager", certManagerExe),
|
||||
Path.Combine(TizenRootPath, "tools", "certificate-manager", certManagerExe)
|
||||
};
|
||||
|
||||
bool certManagerOk = certManagerPaths.Any(File.Exists);
|
||||
|
||||
string certManagerPluginsPath = Path.Combine(TizenRootPath, "tools", "certificate-manager", "plugins");
|
||||
string idePluginsPath = Path.Combine(TizenRootPath, "ide", "plugins");
|
||||
|
||||
bool certExtensionOk =
|
||||
FolderHasCertJar(certManagerPluginsPath) ||
|
||||
FolderHasCertJar(idePluginsPath);
|
||||
|
||||
if (cliOk && cryptoOk && certManagerOk && certExtensionOk)
|
||||
return (TizenDataPath, TizenCypto);
|
||||
}
|
||||
|
||||
string tizenInstallationPath = await InstallMinimalCli();
|
||||
InitializeTizenPaths();
|
||||
return (tizenInstallationPath, TizenCypto);
|
||||
return OperatingSystem.IsWindows() ? $"TizenSdb_{version}.exe" :
|
||||
OperatingSystem.IsLinux() ? $"TizenSdb_{version}_linux" :
|
||||
OperatingSystem.IsMacOS() ? $"TizenSdb_{version}_macos" :
|
||||
throw new PlatformNotSupportedException("Unsupported OS");
|
||||
}
|
||||
private static bool FolderHasCertJar(string path)
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
return false;
|
||||
|
||||
return Directory.EnumerateFiles(path, "*.jar")
|
||||
.Any(file => Path.GetFileName(file)
|
||||
.StartsWith("org.tizen.common.cert_", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
public async Task<bool> ConnectToTvAsync(string tvIpAddress)
|
||||
private bool ShouldUpdateBinary(string existingFilePath, string latestVersion)
|
||||
{
|
||||
if (TizenSdbPath is null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _processHelper.RunCommandAsync(TizenSdbPath, $"connect {tvIpAddress}");
|
||||
return result.Output.Contains($"connected to {tvIpAddress}");
|
||||
// Extract version from filename (e.g., "TizenSdb_v1.0.1.exe" -> "v1.0.1")
|
||||
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(existingFilePath);
|
||||
var match = System.Text.RegularExpressions.Regex.Match(fileNameWithoutExtension, @"_([v]?\d+\.\d+\.\d+)");
|
||||
|
||||
if (!match.Success)
|
||||
return true; // If we can't parse version, update to be safe
|
||||
|
||||
string currentVersion = match.Groups[1].Value;
|
||||
return IsVersionGreater(latestVersion, currentVersion);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
return true; // If anything fails, update to be safe
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsVersionGreater(string latestVersion, string currentVersion)
|
||||
{
|
||||
// Remove 'v' prefix if present for comparison
|
||||
var latest = Version.TryParse(latestVersion.TrimStart('v'), out var latestVer) ? latestVer : null;
|
||||
var current = Version.TryParse(currentVersion.TrimStart('v'), out var currentVer) ? currentVer : null;
|
||||
|
||||
if (latest == null || current == null)
|
||||
return false;
|
||||
|
||||
return latest > current;
|
||||
}
|
||||
|
||||
private async Task<string> GetLatestTizenSdbVersionAsync()
|
||||
{
|
||||
var json = await _httpClient.GetStringAsync(AppSettings.Default.TizenSdb);
|
||||
var releases = JsonConvert.DeserializeObject<List<GitHubRelease>>(json);
|
||||
var firstRelease = releases.FirstOrDefault();
|
||||
|
||||
if (firstRelease == null)
|
||||
throw new InvalidOperationException("No releases found");
|
||||
|
||||
return firstRelease.TagName ?? "v1.0.0";
|
||||
}
|
||||
|
||||
public async Task<string> DownloadTizenSdbAsync(string version = null)
|
||||
{
|
||||
var json = await _httpClient.GetStringAsync(AppSettings.Default.TizenSdb);
|
||||
var releases = JsonConvert.DeserializeObject<List<GitHubRelease>>(json);
|
||||
var firstRelease = releases.FirstOrDefault();
|
||||
|
||||
if (firstRelease == null)
|
||||
throw new InvalidOperationException("No releases found");
|
||||
|
||||
string nameMatch =
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "exe" :
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "linux" :
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "macos" :
|
||||
throw new PlatformNotSupportedException();
|
||||
|
||||
var matchedAsset = firstRelease.Assets
|
||||
.FirstOrDefault(a => !string.IsNullOrEmpty(a.FileName) &&
|
||||
a.FileName.Contains(nameMatch, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (matchedAsset == null)
|
||||
throw new InvalidOperationException($"No matching asset found for {nameMatch}");
|
||||
|
||||
return await DownloadPackageAsync(matchedAsset.DownloadUrl);
|
||||
}
|
||||
|
||||
public async Task<string> DownloadPackageAsync(string downloadUrl)
|
||||
{
|
||||
var fileName = Path.GetFileName(new Uri(downloadUrl).LocalPath);
|
||||
var localPath = Path.Combine(_downloadDirectory, fileName);
|
||||
var localPath = Path.Combine(AppSettings.DownloadPath, fileName);
|
||||
|
||||
if (File.Exists(localPath))
|
||||
return localPath;
|
||||
|
||||
Directory.CreateDirectory(_downloadDirectory);
|
||||
Directory.CreateDirectory(AppSettings.DownloadPath);
|
||||
|
||||
using var response = await _httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
@@ -263,15 +185,18 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
|
||||
return localPath;
|
||||
}
|
||||
|
||||
|
||||
public async Task<InstallResult> InstallPackageAsync(string packageUrl, string tvIpAddress, ProgressCallback? progress = null)
|
||||
{
|
||||
if (TizenCliPath is null || TizenSdbPath is null)
|
||||
if (TizenSdbPath is null)
|
||||
{
|
||||
progress?.Invoke("PleaseInstallTizen".Localized());
|
||||
await _dialogService.ShowErrorAsync("PleaseInstallTizen".Localized());
|
||||
return InstallResult.FailureResult("PleaseInstallTizen".Localized());
|
||||
progress?.Invoke("InstallTizenSdb".Localized());
|
||||
await EnsureTizenSdbAvailable();
|
||||
|
||||
if(TizenSdbPath is null)
|
||||
{
|
||||
await _dialogService.ShowErrorAsync("FailedTizenSdb".Localized());
|
||||
return InstallResult.FailureResult("InstallTizenSdb".Localized());
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
@@ -284,14 +209,14 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
return InstallResult.FailureResult("TvNameNotFound".Localized());
|
||||
}
|
||||
|
||||
string tvDuid = await GetTvDuidAsync();
|
||||
string tvDuid = await GetTvDuidAsync(tvIpAddress);
|
||||
if (string.IsNullOrEmpty(tvDuid))
|
||||
{
|
||||
progress?.Invoke("TvDuidNotFound".Localized());
|
||||
return InstallResult.FailureResult("TvDuidNotFound".Localized());
|
||||
}
|
||||
|
||||
string tizenOs = await FetchTizenOsAsync();
|
||||
string tizenOs = await FetchTizenOsAsync(tvIpAddress);
|
||||
|
||||
if (string.IsNullOrEmpty(tizenOs))
|
||||
tizenOs = "7.0";
|
||||
@@ -305,15 +230,22 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
if (!string.IsNullOrEmpty(tvName))
|
||||
{
|
||||
AppSettings.Default.PermitInstall = true;
|
||||
allowPermitInstall(tvName);
|
||||
await AllowPermitInstall(tvName);
|
||||
}
|
||||
}
|
||||
|
||||
string authorp12 = string.Empty;
|
||||
string distributorp12 = string.Empty;
|
||||
string p12Password = string.Empty;
|
||||
|
||||
if (tizenVersion >= certVersion || AppSettings.Default.ConfigUpdateMode != "None" || AppSettings.Default.ForceSamsungLogin)
|
||||
{
|
||||
string selectedCertificate = _appSettings.Certificate;
|
||||
var certDuid = _appSettings.ChosenCertificates?.Duid;
|
||||
|
||||
Debug.WriteLine($"tvDuid = {tvDuid}");
|
||||
Debug.WriteLine($"certDuid = {certDuid}");
|
||||
|
||||
if (string.IsNullOrEmpty(selectedCertificate) || selectedCertificate == "Jelly2Sams (default)" || tvDuid != certDuid)
|
||||
{
|
||||
progress?.Invoke("SamsungLogin".Localized());;
|
||||
@@ -322,21 +254,18 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
{
|
||||
progress?.Invoke("CreatingCertificateProfile".Localized());
|
||||
var certificateService = new TizenCertificateService(_httpClient, _dialogService);
|
||||
(string p12Location, string p12Password) = await certificateService.GenerateProfileAsync(
|
||||
(authorp12, distributorp12, p12Password) = await certificateService.GenerateProfileAsync(
|
||||
duid: tvDuid,
|
||||
accessToken: auth.access_token,
|
||||
userId: auth.userId,
|
||||
userEmail: auth.inputEmailID,
|
||||
outputPath: Path.Combine(Environment.CurrentDirectory, "Assets", "TizenProfile"),
|
||||
TizenPluginPath ?? string.Empty,
|
||||
outputPath: Path.Combine(AppSettings.CertificatePath, "Jelly2Sams"),
|
||||
progress
|
||||
);
|
||||
|
||||
PackageCertificate = "Jelly2Sams";
|
||||
_appSettings.Certificate = PackageCertificate;
|
||||
_appSettings.Save();
|
||||
|
||||
UpdateCertificateManager(p12Location, p12Password, "Jelly2Sams");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -352,17 +281,17 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
else
|
||||
{
|
||||
progress?.Invoke("UpdatingCertificateProfile".Localized());
|
||||
UpdateCertificateManager("custom", "custom", "custom_jelly");
|
||||
PackageCertificate = "custom_jelly";
|
||||
authorp12 = Path.Combine(AppSettings.ProfilePath, "legacy", "author.p12");
|
||||
distributorp12 = Path.Combine(AppSettings.ProfilePath, "legacy", "tizen-distributor-signer-new.p12");
|
||||
p12Password = "tizenpkcs12passfordsigner";
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Jellyfin IP: {AppSettings.Default.JellyfinIP}");
|
||||
Debug.WriteLine($"Update mode: {AppSettings.Default.ConfigUpdateMode}");
|
||||
if (!string.IsNullOrEmpty(AppSettings.Default.JellyfinIP) && !AppSettings.Default.ConfigUpdateMode.Contains("None"))
|
||||
{
|
||||
string[] userIds = [];
|
||||
|
||||
if (AppSettings.Default.JellyfinUserId == "everyone" && (AppSettings.Default.ConfigUpdateMode != "Server Settings"))
|
||||
if (AppSettings.Default.JellyfinUserId == "everyone" && AppSettings.Default.ConfigUpdateMode != "Server Settings")
|
||||
userIds = [.. (await _jellyfinHelper.LoadJellyfinUsersAsync()).Select(u => u.Id)];
|
||||
else
|
||||
userIds = [AppSettings.Default.JellyfinUserId];
|
||||
@@ -371,7 +300,7 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
AppSettings.Default.ConfigUpdateMode.Contains("Browser") ||
|
||||
AppSettings.Default.ConfigUpdateMode.Contains("All"))
|
||||
{
|
||||
await _jellyfinHelper.ApplyConfigAndResignPackageAsync(TizenCliPath, packageUrl, PackageCertificate, userIds);
|
||||
await _jellyfinHelper.ApplyJellyfinConfigAsync(packageUrl, userIds);
|
||||
}
|
||||
|
||||
|
||||
@@ -384,27 +313,19 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
}
|
||||
|
||||
progress?.Invoke("packageAndSign".Localized());
|
||||
string packageExt = Path.GetExtension(packageUrl).TrimStart('.').ToLowerInvariant();
|
||||
if(OperatingSystem.IsWindows())
|
||||
await _processHelper.RunCommandCmdAsync(TizenCliPath, $"package -t {packageExt} -s {PackageCertificate} -- \"{packageUrl}\"");
|
||||
else
|
||||
await _processHelper.RunCommandAsync(TizenCliPath, $"package -t {packageExt} -s {PackageCertificate} -- \"{packageUrl}\"");
|
||||
await ResignPackageAsync(packageUrl, authorp12, distributorp12, p12Password);
|
||||
|
||||
progress?.Invoke("InstallingPackage".Localized());
|
||||
var installOutput = new ProcessResult();
|
||||
if (OperatingSystem.IsWindows())
|
||||
installOutput = await _processHelper.RunCommandCmdAsync(TizenCliPath, $"install -n \"{packageUrl}\" -t {tvName}");
|
||||
else
|
||||
installOutput = await _processHelper.RunCommandAsync(TizenCliPath, $"install -n \"{packageUrl}\" -t {tvName}");
|
||||
var installOutput = await InstallPackageAsync(tvIpAddress, packageUrl);
|
||||
|
||||
if (File.Exists(packageUrl) && !installOutput.Output.Contains("Failed"))
|
||||
if (File.Exists(packageUrl) && !installOutput.Contains("Failed"))
|
||||
{
|
||||
progress?.Invoke("InstallationSuccessful".Localized());
|
||||
return InstallResult.SuccessResult();
|
||||
}
|
||||
|
||||
progress?.Invoke("InstallationFailed".Localized());
|
||||
return InstallResult.FailureResult($"Installation failed: {installOutput.Output}");
|
||||
return InstallResult.FailureResult($"Installation failed: {installOutput}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -419,440 +340,45 @@ namespace Jellyfin2SamsungCrossOS.Services
|
||||
}
|
||||
public async Task<string> GetTvNameAsync(string tvIpAddress)
|
||||
{
|
||||
if (TizenSdbPath is null)
|
||||
return string.Empty;
|
||||
|
||||
await ConnectToTvAsync(tvIpAddress);
|
||||
var output = await _processHelper.RunCommandAsync(TizenSdbPath, "devices");
|
||||
var match = Regex.Match(output.Output, @"(?<=\n)([^\s]+)\s+device\s+(?<name>[^\s]+)");
|
||||
return match.Success ? match.Groups["name"].Value.Trim() : string.Empty;
|
||||
var output = await _processHelper.RunCommandAsync(TizenSdbPath!, $"devices {tvIpAddress}");
|
||||
var deviceName = output.Output
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault()?.Trim() ?? string.Empty;
|
||||
|
||||
return deviceName;
|
||||
}
|
||||
private async Task<string> FetchTizenOsAsync()
|
||||
private async Task<string> FetchTizenOsAsync(string tvIpAddress)
|
||||
{
|
||||
var output = await _processHelper.RunCommandAsync(TizenSdbPath, "capability");
|
||||
var output = await _processHelper.RunCommandAsync(TizenSdbPath!, $"capability {tvIpAddress}");
|
||||
var match = Regex.Match(output.Output, @"platform_version:([\d.]+)");
|
||||
return match.Success ? match.Groups[1].Value.Trim() : "";
|
||||
}
|
||||
private async Task<string> GetTvDuidAsync()
|
||||
private async Task<string> GetTvDuidAsync(string tvIpAddress)
|
||||
{
|
||||
if (TizenSdbPath is null) return string.Empty;
|
||||
var output = await _processHelper.RunCommandAsync(TizenSdbPath, "shell \"0 getduid\"");
|
||||
var result = string.IsNullOrWhiteSpace(output.Output)
|
||||
? await _processHelper.RunCommandAsync(TizenSdbPath, "shell \"/opt/etc/duid-gadget 2 2> /dev/null\"")
|
||||
: output;
|
||||
|
||||
return result.Output.Trim();
|
||||
var output = await _processHelper.RunCommandAsync(TizenSdbPath!, $"duid {tvIpAddress}");
|
||||
var duid = output.Output
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault()?.Trim() ?? string.Empty;
|
||||
|
||||
return duid;
|
||||
}
|
||||
|
||||
private async Task allowPermitInstall(string tvName)
|
||||
private async Task<string> ResignPackageAsync(string packagePath, string authorP12, string distributorP12, string certPass)
|
||||
{
|
||||
await _processHelper.RunCommandAsync(TizenCliPath, $"install-permit -t {tvName}");
|
||||
var output = await _processHelper.RunCommandAsync(TizenSdbPath!, $"resign \"{packagePath}\" \"{authorP12}\" \"{distributorP12}\" {certPass}");
|
||||
return output.ToString();
|
||||
}
|
||||
private async Task<string> InstallPackageAsync(string tvIpAddress, string packagePath)
|
||||
{
|
||||
var output = await _processHelper.RunCommandAsync(TizenSdbPath!, $"install {tvIpAddress} \"{packagePath}\"");
|
||||
return output.ToString();
|
||||
}
|
||||
|
||||
//UPDATE ALLOWPERMITINSTALL
|
||||
private async Task AllowPermitInstall(string tvName)
|
||||
{
|
||||
await _processHelper.RunCommandAsync(TizenSdbPath!, $"install-permit -t {tvName}");
|
||||
Console.WriteLine("This needs to be created, cause can't / won't use TIZEN CRAP");
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
private void UpdateCertificateManager(string p12Location, string p12Password, string profileName)
|
||||
{
|
||||
void Trace(string m) => Debug.WriteLine($"{DateTime.Now:HH:mm:ss.fff} [PROFILES] {m}");
|
||||
var swTotal = Stopwatch.StartNew();
|
||||
|
||||
Trace($"ENTER name='{profileName}', TizenDataPath='{TizenDataPath}'");
|
||||
if (string.IsNullOrEmpty(TizenDataPath))
|
||||
throw new Exception("Tizen data path is not set.");
|
||||
|
||||
|
||||
string dir = Path.GetDirectoryName(TizenDataPath)!;
|
||||
Trace($"Ensure dir exists: '{dir}' (exists={Directory.Exists(dir)})");
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
Trace("Created dir.");
|
||||
}
|
||||
|
||||
XDocument doc;
|
||||
XElement root;
|
||||
|
||||
if (!File.Exists(TizenDataPath))
|
||||
{
|
||||
Trace("profiles.xml NOT found. Creating new XDocument with root + attrs.");
|
||||
root = new XElement("profiles",
|
||||
new XAttribute("active", profileName),
|
||||
new XAttribute("version", "3.1"));
|
||||
doc = new XDocument(new XDeclaration("1.0", "utf-8", "no"), root);
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace("profiles.xml found. Loading XDocument...");
|
||||
doc = XDocument.Load(TizenDataPath);
|
||||
Trace("Loaded XDocument.");
|
||||
root = doc.Element("profiles") ?? new XElement("profiles");
|
||||
if (doc.Root == null)
|
||||
{
|
||||
Trace("doc.Root was null, adding 'profiles' root.");
|
||||
doc.Add(root);
|
||||
}
|
||||
|
||||
if (root.Attribute("version") == null) { Trace("Setting version attr."); root.SetAttributeValue("version", "3.1"); }
|
||||
if (root.Attribute("active") == null) { Trace("Setting active attr."); root.SetAttributeValue("active", profileName); }
|
||||
}
|
||||
|
||||
// Normalize p12 paths
|
||||
Trace($"Normalize p12 paths. p12Location='{p12Location}'");
|
||||
string authorP12 = p12Location.EndsWith(".p12", StringComparison.OrdinalIgnoreCase)
|
||||
? p12Location
|
||||
: Path.Combine(p12Location, "author.p12");
|
||||
|
||||
string distributorP12 = p12Location.EndsWith(".p12", StringComparison.OrdinalIgnoreCase)
|
||||
? Path.Combine(Path.GetDirectoryName(p12Location)!, "distributor.p12")
|
||||
: Path.Combine(p12Location, "distributor.p12");
|
||||
|
||||
Trace($"authorP12='{authorP12}', distributorP12='{distributorP12}'");
|
||||
|
||||
Trace("Building <profile> element...");
|
||||
var profile = new XElement("profile",
|
||||
new XAttribute("name", profileName),
|
||||
new XElement("profileitem",
|
||||
new XAttribute("ca", ""),
|
||||
new XAttribute("distributor", "0"),
|
||||
new XAttribute("key", authorP12),
|
||||
new XAttribute("password", p12Password),
|
||||
new XAttribute("rootca", "")
|
||||
),
|
||||
new XElement("profileitem",
|
||||
new XAttribute("ca", ""),
|
||||
new XAttribute("distributor", "1"),
|
||||
new XAttribute("key", distributorP12),
|
||||
new XAttribute("password", p12Password),
|
||||
new XAttribute("rootca", "")
|
||||
),
|
||||
new XElement("profileitem",
|
||||
new XAttribute("ca", ""),
|
||||
new XAttribute("distributor", "2"),
|
||||
new XAttribute("key", ""),
|
||||
new XAttribute("password", ""),
|
||||
new XAttribute("rootca", "")
|
||||
)
|
||||
);
|
||||
Trace("Built profile element.");
|
||||
|
||||
// Insert / Replace
|
||||
Trace("Searching for existing profile...");
|
||||
var existing = root.Elements("profile").FirstOrDefault(p => (string?)p.Attribute("name") == profileName);
|
||||
if (existing is null)
|
||||
{
|
||||
Trace("Existing profile NOT found. Adding new.");
|
||||
root.Add(profile);
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace("Existing profile found. Replacing.");
|
||||
existing.ReplaceWith(profile);
|
||||
}
|
||||
|
||||
Trace("Setting 'active' attribute on root...");
|
||||
root.SetAttributeValue("active", profileName);
|
||||
|
||||
// Save
|
||||
Trace($"Saving XDocument to '{TizenDataPath}'...");
|
||||
var swSave = Stopwatch.StartNew();
|
||||
doc.Save(TizenDataPath);
|
||||
swSave.Stop();
|
||||
Trace($"Saved profiles.xml in {swSave.ElapsedMilliseconds} ms.");
|
||||
|
||||
swTotal.Stop();
|
||||
Trace($"EXIT after {swTotal.ElapsedMilliseconds} ms.");
|
||||
}
|
||||
|
||||
private static string? FindTizenRoot()
|
||||
{
|
||||
foreach (var basePath in PossibleTizenPaths)
|
||||
{
|
||||
if (string.IsNullOrEmpty(basePath))
|
||||
continue;
|
||||
|
||||
string tizenExecutable = OperatingSystem.IsWindows() ? "tizen.bat" : "tizen";
|
||||
|
||||
var possiblePath = Path.Combine(basePath, "tools", "ide", "bin", tizenExecutable);
|
||||
if (File.Exists(possiblePath))
|
||||
return basePath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
private async Task<string> InstallMinimalCli()
|
||||
{
|
||||
string installerPath = null;
|
||||
InstallingWindow installingWindow = null;
|
||||
|
||||
try
|
||||
{
|
||||
// 1️⃣ Determine CLI URL
|
||||
string cliUrl = OperatingSystem.IsWindows() ? AppSettings.Default.TizenCliWindows :
|
||||
OperatingSystem.IsLinux() ? AppSettings.Default.TizenCliLinux :
|
||||
OperatingSystem.IsMacOS() ? AppSettings.Default.TizenCliMac :
|
||||
throw new PlatformNotSupportedException("Unsupported OS");
|
||||
|
||||
string installPath = _osHelper.GetInstallPath();
|
||||
|
||||
// 2️⃣ Ask user for confirmation
|
||||
bool userConfirmed = await _dialogService.ShowConfirmationAsync(
|
||||
"minimalCliTitle".Localized(),
|
||||
"minimalCliMessage".Localized(),
|
||||
"keyContinue".Localized(),
|
||||
"keyStop".Localized());
|
||||
|
||||
if (!userConfirmed)
|
||||
return "minimalCliStop".Localized();
|
||||
|
||||
// 3️⃣ Show installing window
|
||||
installingWindow = new InstallingWindow
|
||||
{
|
||||
WindowStartupLocation = Avalonia.Controls.WindowStartupLocation.CenterScreen
|
||||
};
|
||||
installingWindow.Show();
|
||||
|
||||
// 4️⃣ Download installer
|
||||
installingWindow.ViewModel.StatusText = "Downloading Tizen CLI...";
|
||||
|
||||
|
||||
installerPath = await DownloadPackageAsync(cliUrl);
|
||||
|
||||
if (!Directory.Exists(installPath))
|
||||
Directory.CreateDirectory(installPath);
|
||||
|
||||
// 5️⃣ Install Tizen CLI
|
||||
|
||||
installingWindow.ViewModel.StatusText = "Installing Tizen CLI...";
|
||||
|
||||
|
||||
bool cliInstalled = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = installerPath,
|
||||
Arguments = $"--accept-license \"{installPath}\"",
|
||||
UseShellExecute = true,
|
||||
CreateNoWindow = false,
|
||||
Verb = "runas"
|
||||
};
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
cliInstalled = process.ExitCode == 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
await _processHelper.RunCommandAsync("chmod", $"+x \"{installerPath}\"");
|
||||
var result = await _processHelper.RunCommandAsync("bash", $"\"{installerPath}\" --accept-license \"{installPath}\"");
|
||||
cliInstalled = result.ExitCode == 0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Tizen CLI installation failed: {ex}");
|
||||
}
|
||||
|
||||
if (!cliInstalled)
|
||||
return "Tizen CLI installation failed.";
|
||||
|
||||
installingWindow.ViewModel.StatusText = "Installing Tizen Certificate tooling...";
|
||||
|
||||
bool certInstalled = await InstallSamsungCertificateExtensionAsync(installPath, installingWindow);
|
||||
|
||||
if (!certInstalled)
|
||||
{
|
||||
bool retry = await _dialogService.ShowConfirmationAsync(
|
||||
"InstallationFailed".Localized(),
|
||||
"ReInstallingCertificateManager".Localized(),
|
||||
"keyYes".Localized(),
|
||||
"keyNo".Localized(),
|
||||
owner: installingWindow
|
||||
);
|
||||
|
||||
if (!retry)
|
||||
return "certFailed".Localized();
|
||||
|
||||
certInstalled = await InstallSamsungCertificateExtensionAsync(installPath, installingWindow);
|
||||
if (!certInstalled)
|
||||
return "certRetryFailed".Localized();
|
||||
}
|
||||
|
||||
// 8️⃣ Set Tizen paths
|
||||
var tizenRoot = FindTizenRoot() ?? string.Empty;
|
||||
TizenCliPath = OperatingSystem.IsWindows()
|
||||
? Path.Combine(tizenRoot, "tools", "ide", "bin", "tizen.bat")
|
||||
: Path.Combine(tizenRoot, "tools", "ide", "bin", "tizen");
|
||||
|
||||
TizenSdbPath = OperatingSystem.IsWindows()
|
||||
? Path.Combine(tizenRoot, "tools", "sdb.exe")
|
||||
: Path.Combine(tizenRoot, "tools", "sdb");
|
||||
|
||||
return !string.IsNullOrEmpty(tizenRoot)
|
||||
? TizenCliPath
|
||||
: "Tizen root folder not found after installation.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"An error occurred during installation: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (installingWindow != null)
|
||||
await Dispatcher.UIThread.InvokeAsync(() => installingWindow.Close());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> InstallSamsungCertificateExtensionAsync(string installPath, InstallingWindow installingWindow)
|
||||
{
|
||||
string certManagerExe = OperatingSystem.IsWindows() ? "certificate-manager.exe" : "certificate-manager.bin";
|
||||
string[] possiblePaths = {
|
||||
Path.Combine(installPath, "tools", "certificate-manager", certManagerExe),
|
||||
Path.Combine(installPath, "certificate-manager", certManagerExe)
|
||||
};
|
||||
|
||||
// Already installed?
|
||||
if (possiblePaths.Any(File.Exists))
|
||||
return true;
|
||||
|
||||
string packageManagerExe = OperatingSystem.IsWindows() ? "package-manager-cli.exe" : "package-manager-cli.bin";
|
||||
string packageManagerPath = Path.Combine(installPath, "package-manager", packageManagerExe);
|
||||
|
||||
if (!File.Exists(packageManagerPath))
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
installingWindow.ViewModel.SetStatusText("Package manager CLI not found. Please ensure Tizen Studio is properly installed.")
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
await EnsureTizenExtensionsEnabledAsync(installPath, packageManagerPath, installingWindow);
|
||||
|
||||
try
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// ---- Certificate Manager ----
|
||||
installingWindow.ViewModel.SetStatusText("Installing Certificate Manager...");
|
||||
var certProcessInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = packageManagerPath,
|
||||
Arguments = "install \"Certificate-Manager\" --accept-license",
|
||||
UseShellExecute = true,
|
||||
CreateNoWindow = false,
|
||||
WorkingDirectory = installPath
|
||||
};
|
||||
|
||||
using var certProcess = Process.Start(certProcessInfo);
|
||||
await certProcess.WaitForExitAsync();
|
||||
if (certProcess.ExitCode != 0)
|
||||
return false;
|
||||
|
||||
// ---- Cert Add-On ----
|
||||
installingWindow.ViewModel.SetStatusText("Installing Certificate Add-On...");
|
||||
var addOnProcessInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = packageManagerPath,
|
||||
Arguments = "install \"cert-add-on\" --accept-license",
|
||||
UseShellExecute = true,
|
||||
CreateNoWindow = false,
|
||||
WorkingDirectory = installPath
|
||||
};
|
||||
|
||||
using var addOnProcess = Process.Start(addOnProcessInfo);
|
||||
await addOnProcess.WaitForExitAsync();
|
||||
if (addOnProcess.ExitCode != 0)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Linux/macOS CLI-based installation
|
||||
installingWindow.ViewModel.SetStatusText("Installing Certificate Manager...");
|
||||
await _processHelper.RunCommandAsync(packageManagerPath, "install Certificate-Manager --accept-license");
|
||||
|
||||
installingWindow.ViewModel.SetStatusText("Installing Certificate Add-On...");
|
||||
await _processHelper.RunCommandAsync(packageManagerPath, "install cert-add-on --accept-license");
|
||||
}
|
||||
|
||||
// Verify installation
|
||||
if (possiblePaths.Any(File.Exists))
|
||||
return true;
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
installingWindow.ViewModel.SetStatusText("Installation completed but certificate manager executable not found.")
|
||||
);
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
installingWindow.ViewModel.SetStatusText($"Samsung Certificate Extension installation failed: {ex.Message}")
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public async Task EnsureTizenExtensionsEnabledAsync(string installPath, string packageManagerPath, InstallingWindow installingWindow)
|
||||
{
|
||||
installingWindow.ViewModel.SetStatusText("CheckingPackageManagerList".Localized());
|
||||
|
||||
var result = OperatingSystem.IsWindows()
|
||||
? await _processHelper.RunElevatedAndCaptureOutputAsync(packageManagerPath, "extra --list --detail", installPath)
|
||||
: await _processHelper.RunCommandAsync(packageManagerPath, "extra --list --detail", installPath);
|
||||
|
||||
string output = result?.Output ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
installingWindow.ViewModel.SetStatusText("Failed to retrieve extension list.");
|
||||
throw new InvalidOperationException("Failed to get extension output.");
|
||||
}
|
||||
|
||||
var extensions = _fileHelper.ParseExtensions(output);
|
||||
var targets = new[] { "Samsung Certificate Extension", "Samsung Tizen TV SDK" };
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var ext = extensions.FirstOrDefault(e => e.Name.Equals(target, StringComparison.OrdinalIgnoreCase));
|
||||
if (ext == null)
|
||||
{
|
||||
installingWindow.ViewModel.SetStatusText($"Extension '{target}' not found.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ext.Activated)
|
||||
{
|
||||
installingWindow.ViewModel.SetStatusText($"Extension '{target}' already active.");
|
||||
}
|
||||
else
|
||||
{
|
||||
installingWindow.ViewModel.SetStatusText($"Activating extension: {target}...");
|
||||
|
||||
var args = $"extra -act {ext.Index}";
|
||||
|
||||
var activationResult = OperatingSystem.IsWindows()
|
||||
? await _processHelper.RunElevatedAndCaptureOutputAsync(packageManagerPath, args, installPath)
|
||||
: await _processHelper.RunCommandAsync(packageManagerPath, args, installPath);
|
||||
|
||||
string activationOutput = activationResult?.Output ?? string.Empty;
|
||||
|
||||
|
||||
if (activationOutput.Contains("activated", StringComparison.OrdinalIgnoreCase) ||
|
||||
activationOutput.Contains("success", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
installingWindow.ViewModel.SetStatusText($"Activated: {target}");
|
||||
}
|
||||
else
|
||||
{
|
||||
installingWindow.ViewModel.SetStatusText($"Failed to activate {target}.");
|
||||
throw new InvalidOperationException($"Failed to activate extension {target}. Output: {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Jellyfin2SamsungCrossOS.ViewModels;
|
||||
using Jellyfin2Samsung.ViewModels;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS
|
||||
namespace Jellyfin2Samsung
|
||||
{
|
||||
public class ViewLocator : IDataTemplate
|
||||
{
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Jellyfin2SamsungCrossOS.Services;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.ViewModels
|
||||
namespace Jellyfin2Samsung.ViewModels
|
||||
{
|
||||
public partial class InstallationCompleteViewModel : ObservableObject
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.ViewModels
|
||||
namespace Jellyfin2Samsung.ViewModels
|
||||
{
|
||||
public partial class InstallingWindowViewModel : ObservableObject
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Jellyfin2SamsungCrossOS.Services;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using System;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.ViewModels
|
||||
namespace Jellyfin2Samsung.ViewModels
|
||||
{
|
||||
public partial class IpInputDialogViewModel : ObservableObject
|
||||
{
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Jellyfin2SamsungCrossOS.Helpers;
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2SamsungCrossOS.Services;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Jellyfin2Samsung.ViewModels;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.ViewModels
|
||||
namespace Jellyfin2Samsung.ViewModels
|
||||
{
|
||||
public partial class JellyfinConfigViewModel : ViewModelBase
|
||||
{
|
||||
|
||||
@@ -3,9 +3,9 @@ using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Jellyfin2SamsungCrossOS.Helpers;
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2SamsungCrossOS.Services;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
@@ -17,7 +17,7 @@ using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.ViewModels
|
||||
namespace Jellyfin2Samsung.ViewModels
|
||||
{
|
||||
public partial class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
@@ -162,14 +162,14 @@ namespace Jellyfin2SamsungCrossOS.ViewModels
|
||||
{
|
||||
try
|
||||
{
|
||||
SetStatus("CheckingTizenCli");
|
||||
SetStatus("CheckingTizenSdb");
|
||||
|
||||
|
||||
var (tizenDataPath, tizenCliPath) = await _tizenInstaller.EnsureTizenCliAvailable();
|
||||
string tizenSdb = await _tizenInstaller.EnsureTizenSdbAvailable();
|
||||
|
||||
if (string.IsNullOrEmpty(tizenDataPath))
|
||||
if (string.IsNullOrEmpty(tizenSdb))
|
||||
{
|
||||
SetStatus("TizenCliFailed");
|
||||
SetStatus("FailedTizenSdb");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@ namespace Jellyfin2SamsungCrossOS.ViewModels
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetStatus("InitializationFailed");
|
||||
await _dialogService.ShowErrorAsync($"{L("InitializationFailed")} {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Jellyfin2SamsungCrossOS.Helpers;
|
||||
using Jellyfin2SamsungCrossOS.Models;
|
||||
using Jellyfin2SamsungCrossOS.Services;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.ViewModels
|
||||
namespace Jellyfin2Samsung.ViewModels
|
||||
{
|
||||
public partial class SettingsViewModel : ViewModelBase
|
||||
{
|
||||
@@ -237,8 +236,7 @@ namespace Jellyfin2SamsungCrossOS.ViewModels
|
||||
|
||||
private async Task InitializeCertificatesAsync()
|
||||
{
|
||||
var (profilePath, tizenCrypto) = await _tizenService.EnsureTizenCliAvailable();
|
||||
var certificates = _certificateHelper.GetAvailableCertificates(profilePath, tizenCrypto);
|
||||
var certificates = _certificateHelper.GetAvailableCertificates(AppSettings.CertificatePath);
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
@@ -249,16 +247,25 @@ namespace Jellyfin2SamsungCrossOS.ViewModels
|
||||
ExistingCertificates? selectedCert = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(savedCertName))
|
||||
selectedCert = AvailableCertificates.FirstOrDefault(c => c.Name == savedCertName);
|
||||
{
|
||||
selectedCert = AvailableCertificates
|
||||
.FirstOrDefault(c => c.Name == savedCertName);
|
||||
}
|
||||
|
||||
selectedCert ??= AvailableCertificates.FirstOrDefault(c => c.Name == "Jelly2Sams (default)")
|
||||
?? AvailableCertificates.FirstOrDefault();
|
||||
selectedCert ??= AvailableCertificates
|
||||
.FirstOrDefault(c => c.Name == "Jelly2Sams");
|
||||
|
||||
selectedCert ??= AvailableCertificates
|
||||
.FirstOrDefault(c => c.Name == "Jelly2Sams (default)");
|
||||
|
||||
selectedCert ??= AvailableCertificates.FirstOrDefault();
|
||||
|
||||
if (selectedCert != null)
|
||||
SelectedCertificate = selectedCert.Name;
|
||||
|
||||
AppSettings.Default.ChosenCertificates = selectedCert;
|
||||
});
|
||||
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.ViewModels
|
||||
namespace Jellyfin2Samsung.ViewModels
|
||||
{
|
||||
public class ViewModelBase : ObservableObject
|
||||
{
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:viewModels="clr-namespace:Jellyfin2SamsungCrossOS.ViewModels"
|
||||
xmlns:viewModels="clr-namespace:Jellyfin2Samsung.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Jellyfin2SamsungCrossOS.InstallationCompleteWindow"
|
||||
x:Class="Jellyfin2Samsung.InstallationCompleteWindow"
|
||||
x:DataType="viewModels:InstallationCompleteViewModel"
|
||||
Title="{Binding Title}"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
@@ -19,9 +19,9 @@
|
||||
<Window.Styles>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentTheme.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2SamsungCrossOS/Styles/Buttons.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2SamsungCrossOS/Styles/TextBlocks.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2SamsungCrossOS/Styles/ComboBoxes.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2Samsung/Styles/Buttons.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2Samsung/Styles/TextBlocks.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2Samsung/Styles/ComboBoxes.axaml"/>
|
||||
</Window.Styles>
|
||||
|
||||
<Border Background="White"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Jellyfin2SamsungCrossOS.ViewModels;
|
||||
using Jellyfin2Samsung.ViewModels;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS;
|
||||
namespace Jellyfin2Samsung;
|
||||
|
||||
public partial class InstallationCompleteWindow : Window
|
||||
{
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Jellyfin2SamsungCrossOS.InstallingWindow"
|
||||
x:Class="Jellyfin2Samsung.InstallingWindow"
|
||||
x:DataType="viewModels:InstallingWindowViewModel"
|
||||
xmlns:viewModels="clr-namespace:Jellyfin2SamsungCrossOS.ViewModels"
|
||||
xmlns:viewModels="clr-namespace:Jellyfin2Samsung.ViewModels"
|
||||
Width="300" Height="120"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Jellyfin2SamsungCrossOS.ViewModels;
|
||||
using Jellyfin2Samsung.ViewModels;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS;
|
||||
namespace Jellyfin2Samsung;
|
||||
|
||||
public partial class InstallingWindow : Window
|
||||
{
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:viewModels="clr-namespace:Jellyfin2SamsungCrossOS.ViewModels"
|
||||
xmlns:viewModels="clr-namespace:Jellyfin2Samsung.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="420"
|
||||
d:DesignHeight="220"
|
||||
x:Class="Jellyfin2SamsungCrossOS.IpInputDialog"
|
||||
x:Class="Jellyfin2Samsung.IpInputDialog"
|
||||
x:DataType="viewModels:IpInputDialogViewModel"
|
||||
Width="420"
|
||||
Height="220"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
using Jellyfin2SamsungCrossOS.Services;
|
||||
using Jellyfin2SamsungCrossOS.ViewModels;
|
||||
using Jellyfin2Samsung;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS;
|
||||
namespace Jellyfin2Samsung;
|
||||
|
||||
public partial class IpInputDialog : Window
|
||||
{
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:viewModels="clr-namespace:Jellyfin2SamsungCrossOS.ViewModels"
|
||||
xmlns:viewModels="clr-namespace:Jellyfin2Samsung.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Jellyfin2SamsungCrossOS.JellyfinConfigView"
|
||||
x:Class="Jellyfin2Samsung.JellyfinConfigView"
|
||||
x:DataType="viewModels:JellyfinConfigViewModel"
|
||||
Title="Jellyfin Config"
|
||||
Width="600"
|
||||
@@ -14,9 +14,9 @@
|
||||
<Window.Styles>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentTheme.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2SamsungCrossOS/Styles/Buttons.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2SamsungCrossOS/Styles/TextBlocks.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2SamsungCrossOS/Styles/ComboBoxes.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2Samsung/Styles/Buttons.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2Samsung/Styles/TextBlocks.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2Samsung/Styles/ComboBoxes.axaml"/>
|
||||
</Window.Styles>
|
||||
|
||||
<Border Background="White"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Jellyfin2SamsungCrossOS.ViewModels;
|
||||
using Jellyfin2Samsung.ViewModels;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS;
|
||||
namespace Jellyfin2Samsung;
|
||||
|
||||
public partial class JellyfinConfigView : Window
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Jellyfin2SamsungCrossOS.Views.MainWindow"
|
||||
x:Class="Jellyfin2Samsung.Views.MainWindow"
|
||||
x:DataType="viewModels:MainWindowViewModel"
|
||||
xmlns:viewModels="clr-namespace:Jellyfin2SamsungCrossOS.ViewModels"
|
||||
xmlns:viewModels="clr-namespace:Jellyfin2Samsung.ViewModels"
|
||||
xmlns:fa="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
Width="580"
|
||||
Height="410"
|
||||
@@ -13,9 +13,9 @@
|
||||
<Window.Styles>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentTheme.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2SamsungCrossOS/Styles/Buttons.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2SamsungCrossOS/Styles/TextBlocks.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2SamsungCrossOS/Styles/ComboBoxes.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2Samsung/Styles/Buttons.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2Samsung/Styles/TextBlocks.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2Samsung/Styles/ComboBoxes.axaml"/>
|
||||
</Window.Styles>
|
||||
|
||||
<Border Background="White"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Avalonia.Controls;
|
||||
using Jellyfin2SamsungCrossOS.ViewModels;
|
||||
using Jellyfin2Samsung.ViewModels;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS.Views
|
||||
namespace Jellyfin2Samsung.Views
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:fa="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Jellyfin2SamsungCrossOS.SettingsView"
|
||||
x:Class="Jellyfin2Samsung.SettingsView"
|
||||
x:DataType="viewModels:SettingsViewModel"
|
||||
xmlns:viewModels="clr-namespace:Jellyfin2SamsungCrossOS.ViewModels"
|
||||
xmlns:viewModels="clr-namespace:Jellyfin2Samsung.ViewModels"
|
||||
Title="Settings"
|
||||
Width="610"
|
||||
Height="580"
|
||||
@@ -14,9 +14,9 @@
|
||||
<Window.Styles>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Fluent/FluentTheme.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2SamsungCrossOS/Styles/Buttons.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2SamsungCrossOS/Styles/TextBlocks.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2SamsungCrossOS/Styles/ComboBoxes.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2Samsung/Styles/Buttons.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2Samsung/Styles/TextBlocks.axaml"/>
|
||||
<StyleInclude Source="avares://Jellyfin2Samsung/Styles/ComboBoxes.axaml"/>
|
||||
</Window.Styles>
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Jellyfin2SamsungCrossOS;
|
||||
namespace Jellyfin2Samsung;
|
||||
|
||||
public partial class SettingsView : Window
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- This manifest is used on Windows only.
|
||||
Don't remove it as it might cause problems with window transparency and embedded controls.
|
||||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||
<assemblyIdentity version="1.0.0.0" name="Jellyfin2Samsung_CrossOS.Desktop"/>
|
||||
<assemblyIdentity version="1.0.0.0" name="Jellyfin2Samsung_.Desktop"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
|
||||
@@ -20,7 +20,7 @@ switch -Regex ($betaAnswer.Trim().ToLower()) {
|
||||
$ChannelSuffix = $(if ($IsBeta) { "-beta" } else { "" })
|
||||
|
||||
# ---- Names & paths ----
|
||||
$ProjectName = "Jellyfin2SamsungCrossOS" # executable name produced by dotnet publish
|
||||
$ProjectName = "Jellyfin2Samsung" # executable name produced by dotnet publish
|
||||
$ProductName = "Jellyfin2Samsung" # artifact prefix
|
||||
$OutputRoot = Join-Path $PSScriptRoot "publish"
|
||||
$DistDir = $OutputRoot
|
||||
|
||||
Reference in New Issue
Block a user