removed the need for Tizen Software and moved out of AppData cause no need anymore

This commit is contained in:
PatrickSt1991
2025-10-12 21:07:28 +02:00
parent 653d4f252a
commit 49f353c5a2
69 changed files with 425 additions and 1262 deletions

View File

@@ -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>

View File

@@ -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);

View File

@@ -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...",

View File

@@ -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...",

View File

@@ -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...",

View File

@@ -1,3 +0,0 @@
Do not delete me!
This folder will be used to place the root certificates comming from the .jar files!

View File

@@ -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>

View File

@@ -1,6 +1,6 @@
using Jellyfin2SamsungCrossOS.Services;
using Jellyfin2Samsung.Interfaces;
namespace Jellyfin2SamsungCrossOS.Extensions
namespace Jellyfin2Samsung.Extensions
{
public static class LocalizationExtensions
{

View File

@@ -1,4 +1,4 @@
namespace Jellyfin2SamsungCrossOS.Extensions
namespace Jellyfin2Samsung.Extensions
{
public delegate void ProgressCallback(string message);
}

View File

@@ -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() { }

View File

@@ -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)
{

View File

@@ -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();
}
}
}

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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,
};

View File

@@ -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");
}
}
}
}

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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>

View File

@@ -1,7 +1,7 @@
using Avalonia.Controls;
using System.Threading.Tasks;
namespace Jellyfin2SamsungCrossOS.Services
namespace Jellyfin2Samsung.Interfaces
{
public interface IDialogService
{

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
namespace Jellyfin2SamsungCrossOS.Services
namespace Jellyfin2Samsung.Interfaces
{
public interface ILocalizationService
{

View File

@@ -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
{

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
namespace Jellyfin2SamsungCrossOS.Models
namespace Jellyfin2Samsung.Models
{
internal class DeviceCapabilities
{

View File

@@ -1,7 +1,7 @@
using System;
using System.Globalization;
namespace Jellyfin2SamsungCrossOS.Models
namespace Jellyfin2Samsung.Models
{
public class ExistingCertificates
{

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
namespace Jellyfin2SamsungCrossOS.Models
namespace Jellyfin2Samsung.Models
{
public class GitHubRelease

View File

@@ -1,4 +1,4 @@
namespace Jellyfin2SamsungCrossOS.Models
namespace Jellyfin2Samsung.Models
{
public class InstallResult
{

View File

@@ -1,4 +1,4 @@
namespace Jellyfin2SamsungCrossOS.Models
namespace Jellyfin2Samsung.Models
{
public class JellyfinAuth
{

View File

@@ -1,4 +1,4 @@
namespace Jellyfin2SamsungCrossOS.Models
namespace Jellyfin2Samsung.Models
{
public class LanguageOption
{

View File

@@ -1,4 +1,4 @@
namespace Jellyfin2SamsungCrossOS.Models
namespace Jellyfin2Samsung.Models
{
public class NetworkDevice
{

View File

@@ -1,4 +1,4 @@
namespace Jellyfin2SamsungCrossOS.Models
namespace Jellyfin2Samsung.Models
{
public class ProcessResult
{

View File

@@ -1,4 +1,4 @@
namespace Jellyfin2SamsungCrossOS.Models
namespace Jellyfin2Samsung.Models
{
public class SamsungAuth
{

View File

@@ -1,7 +1,7 @@
using System;
using Avalonia;
namespace Jellyfin2SamsungCrossOS
namespace Jellyfin2Samsung
{
internal sealed class Program
{

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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))

View File

@@ -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
{

View File

@@ -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;
}
}
}

View File

@@ -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}");
}
}
}
}
}
}

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -1,6 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Jellyfin2SamsungCrossOS.ViewModels
namespace Jellyfin2Samsung.ViewModels
{
public partial class InstallingWindowViewModel : ObservableObject
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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}");
}
}

View File

@@ -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()
{

View File

@@ -1,6 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Jellyfin2SamsungCrossOS.ViewModels
namespace Jellyfin2Samsung.ViewModels
{
public class ViewModelBase : ObservableObject
{

View File

@@ -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"

View File

@@ -1,7 +1,7 @@
using Avalonia.Controls;
using Jellyfin2SamsungCrossOS.ViewModels;
using Jellyfin2Samsung.ViewModels;
namespace Jellyfin2SamsungCrossOS;
namespace Jellyfin2Samsung;
public partial class InstallationCompleteWindow : Window
{

View File

@@ -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"

View File

@@ -1,7 +1,7 @@
using Avalonia.Controls;
using Jellyfin2SamsungCrossOS.ViewModels;
using Jellyfin2Samsung.ViewModels;
namespace Jellyfin2SamsungCrossOS;
namespace Jellyfin2Samsung;
public partial class InstallingWindow : Window
{

View File

@@ -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"

View File

@@ -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
{

View File

@@ -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"

View File

@@ -1,7 +1,7 @@
using Avalonia.Controls;
using Jellyfin2SamsungCrossOS.ViewModels;
using Jellyfin2Samsung.ViewModels;
namespace Jellyfin2SamsungCrossOS;
namespace Jellyfin2Samsung;
public partial class JellyfinConfigView : Window
{

View File

@@ -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"

View File

@@ -1,7 +1,7 @@
using Avalonia.Controls;
using Jellyfin2SamsungCrossOS.ViewModels;
using Jellyfin2Samsung.ViewModels;
namespace Jellyfin2SamsungCrossOS.Views
namespace Jellyfin2Samsung.Views
{
public partial class MainWindow : Window
{

View File

@@ -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>

View File

@@ -2,7 +2,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Jellyfin2SamsungCrossOS;
namespace Jellyfin2Samsung;
public partial class SettingsView : Window
{

View File

@@ -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>

View File

@@ -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