Compare commits

...

18 Commits
v1.0 ... v1.5

Author SHA1 Message Date
PatrickSt1991
a549bfee60 set download and install after initialize 2025-05-17 22:03:03 +02:00
PatrickSt1991
69451e62c4 Added status when searching 2025-05-17 21:51:51 +02:00
Patrick
cc163f9957 Merge pull request #5 from jeppevinkel/feature/multi-lingual-support
Multilingual support
2025-05-17 21:42:02 +02:00
Patrick
9d1edc512d Merge branch 'master' into feature/multi-lingual-support 2025-05-17 21:41:40 +02:00
PatrickSt1991
7646daf829 Network scan for samsung device - Thanks jeppevinkel 2025-05-17 21:39:05 +02:00
jeppevinkel
f5ab17f551 Merge branch 'master' into feature/multi-lingual-support 2025-05-17 20:26:43 +02:00
Patrick
f66adead72 Merge pull request #2 from jeppevinkel/feature/automatically-detect-tv
Automatically add Tizen devices to dropdown
2025-05-17 19:43:13 +02:00
Patrick
ff7afbf1ed Update README.md
Credits when credits are due
2025-05-16 23:19:57 +02:00
Jeppe Beier
c7138b10ec Add initial localization draft 2025-05-16 20:58:59 +02:00
PatrickSt1991
68095c6041 fixing #3 2025-05-15 22:30:51 +02:00
PatrickSt1991
b9eeb71fc3 packageUrl fix 2025-05-15 22:30:05 +02:00
Jeppe Beier
ae8dabd5f9 Add name fetching 2025-05-15 19:09:01 +02:00
Jeppe Beier
bde2e81063 Add device refresh button 2025-05-14 23:17:43 +02:00
Jeppe Beier
7aed84eaec Initial implementation of network scanning 2025-05-13 22:50:03 +02:00
PatrickSt1991
4aa659a9a9 Use %LocalAppData%\Programs\TizenStudioCli for non elevated tooling 2025-05-13 22:05:21 +02:00
PatrickSt1991
7402bd7014 - Moved away from RunAs Administrator
- Changed the default install location to C:\TizenStudioCli
2025-05-13 21:13:55 +02:00
PatrickSt1991
5c8406dfde TizenInstaller fix, grep name intead of IP 2025-05-13 11:08:43 +02:00
Patrick
1399282df8 Update README.md 2025-05-13 10:07:54 +02:00
15 changed files with 1186 additions and 61 deletions

View File

@@ -1,7 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Samsung_Jellyfin_Installer.Services;
using Samsung_Jellyfin_Installer.ViewModels;
using System;
using System.Net.Http;
using System.Windows;
@@ -32,6 +31,7 @@ namespace Samsung_Jellyfin_Installer
// Register services
services.AddSingleton<IDialogService, DialogService>();
services.AddSingleton<ITizenInstallerService, TizenInstallerService>();
services.AddSingleton<INetworkService, NetworkService>();
// Register ViewModels
services.AddSingleton<MainWindowViewModel>();

225
Localization/Strings.Designer.cs generated Normal file
View File

@@ -0,0 +1,225 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Samsung_Jellyfin_Installer.Localization {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Strings {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Strings() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Samsung_Jellyfin_Installer.Localization.Strings", typeof(Strings).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Connecting to device....
/// </summary>
public static string ConnectingToDevice {
get {
return ResourceManager.GetString("ConnectingToDevice", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Download &amp; Install.
/// </summary>
public static string DownloadAndInstall {
get {
return ResourceManager.GetString("DownloadAndInstall", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Download failed:.
/// </summary>
public static string DownloadFailed {
get {
return ResourceManager.GetString("DownloadFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Downloading package....
/// </summary>
public static string DownloadingPackage {
get {
return ResourceManager.GetString("DownloadingPackage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to load releases:.
/// </summary>
public static string FailedLoadingReleases {
get {
return ResourceManager.GetString("FailedLoadingReleases", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Installation failed.
/// </summary>
public static string InstallationFailed {
get {
return ResourceManager.GetString("InstallationFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Installation may have failed.
/// </summary>
public static string InstallationMaybeFailed {
get {
return ResourceManager.GetString("InstallationMaybeFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Installation successful.
/// </summary>
public static string InstallationSuccessful {
get {
return ResourceManager.GetString("InstallationSuccessful", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Successfully installed on.
/// </summary>
public static string InstallationSuccessfulOn {
get {
return ResourceManager.GetString("InstallationSuccessfulOn", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Installing package on device....
/// </summary>
public static string InstallingPackage {
get {
return ResourceManager.GetString("InstallingPackage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Loading releases....
/// </summary>
public static string LoadingReleases {
get {
return ResourceManager.GetString("LoadingReleases", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Output.
/// </summary>
public static string Output {
get {
return ResourceManager.GetString("Output", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Packaging the wgt file with certificate....
/// </summary>
public static string PackagingWgtWithCertificate {
get {
return ResourceManager.GetString("PackagingWgtWithCertificate", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tizen CLI is required but not found. Please install Tizen Studio first..
/// </summary>
public static string PleaseInstallTizen {
get {
return ResourceManager.GetString("PleaseInstallTizen", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Retrieving device address....
/// </summary>
public static string RetrievingDeviceAddress {
get {
return ResourceManager.GetString("RetrievingDeviceAddress", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Scanning network for Samsung TV....
/// </summary>
public static string ScanningNetwork {
get {
return ResourceManager.GetString("ScanningNetwork", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to TV Name could not be found....
/// </summary>
public static string TvNameNotFound {
get {
return ResourceManager.GetString("TvNameNotFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Updating certificate profile....
/// </summary>
public static string UpdatingCertificateProfile {
get {
return ResourceManager.GetString("UpdatingCertificateProfile", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="DownloadAndInstall" xml:space="preserve">
<value>Download &amp; Installer</value>
</data>
<data name="InstallationFailed" xml:space="preserve">
<value>Installationen mislykkedes</value>
</data>
<data name="InstallationSuccessfulOn" xml:space="preserve">
<value>Succesfuldt installeret på</value>
</data>
<data name="DownloadFailed" xml:space="preserve">
<value>Download mislykkedes:</value>
</data>
<data name="FailedLoadingReleases" xml:space="preserve">
<value>Kunne ikke indlæse udgivelser:</value>
</data>
<data name="PleaseInstallTizen" xml:space="preserve">
<value>Tizen CLI er påkrævet, men ikke fundet. Installer venligst Tizen Studio først.</value>
</data>
<data name="DownloadingPackage" xml:space="preserve">
<value>Downloader pakke...</value>
</data>
<data name="ConnectingToDevice" xml:space="preserve">
<value>Forbinder til enhed...</value>
</data>
<data name="RetrievingDeviceAddress" xml:space="preserve">
<value>Henter enhedsadresse...</value>
</data>
<data name="TvNameNotFound" xml:space="preserve">
<value>TV navn kunne ikke findes...</value>
</data>
<data name="UpdatingCertificateProfile" xml:space="preserve">
<value>Opdaterer certifikatprofil...</value>
</data>
<data name="PackagingWgtWithCertificate" xml:space="preserve">
<value>Pakker wgt-filen med certifikat...</value>
</data>
<data name="InstallationSuccessful" xml:space="preserve">
<value>Installation vellykket</value>
</data>
<data name="InstallingPackage" xml:space="preserve">
<value>Installerer pakke på enhed...</value>
</data>
<data name="InstallationMaybeFailed" xml:space="preserve">
<value>Installationen er muligvis mislykket</value>
</data>
<data name="Output" xml:space="preserve">
<value>Output</value>
</data>
<data name="LoadingReleases" xml:space="preserve">
<value>Indlæser udgivelser...</value>
</data>
<data name="ScanningNetwork" xml:space="preserve">
<value>Scanner netværk til Samsung TV...</value>
</data>
</root>

View File

@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="DownloadAndInstall" xml:space="preserve">
<value>Download &amp; Installeer</value>
</data>
<data name="InstallationFailed" xml:space="preserve">
<value>Installatie mislukt</value>
</data>
<data name="InstallationSuccessfulOn" xml:space="preserve">
<value>Succesvol geïnstalleerd op</value>
</data>
<data name="DownloadFailed" xml:space="preserve">
<value>Downloaden mislukt:</value>
</data>
<data name="FailedLoadingReleases" xml:space="preserve">
<value>Kon releases niet laden:</value>
</data>
<data name="PleaseInstallTizen" xml:space="preserve">
<value>Tizen CLI is vereist maar niet gevonden. Installeer eerst Tizen Studio.</value>
</data>
<data name="DownloadingPackage" xml:space="preserve">
<value>Pakket downloaden...</value>
</data>
<data name="ConnectingToDevice" xml:space="preserve">
<value>Verbinding maken met apparaat...</value>
</data>
<data name="RetrievingDeviceAddress" xml:space="preserve">
<value>Apparaatadres ophalen...</value>
</data>
<data name="TvNameNotFound" xml:space="preserve">
<value>TV Naam kon niet worden gevonden...</value>
</data>
<data name="UpdatingCertificateProfile" xml:space="preserve">
<value>Certificaatprofiel bijwerken...</value>
</data>
<data name="PackagingWgtWithCertificate" xml:space="preserve">
<value>Het wgt-bestand met certificaat inpakken...</value>
</data>
<data name="InstallationSuccessful" xml:space="preserve">
<value>Installatie geslaagd</value>
</data>
<data name="InstallingPackage" xml:space="preserve">
<value>Pakket installeren op apparaat...</value>
</data>
<data name="InstallationMaybeFailed" xml:space="preserve">
<value>Installatie is mogelijk mislukt</value>
</data>
<data name="Output" xml:space="preserve">
<value>Output</value>
</data>
<data name="LoadingReleases" xml:space="preserve">
<value>Ophalen van releases...</value>
</data>
<data name="ScanningNetwork" xml:space="preserve">
<value>Network scannen voor Samsung TV...</value>
</data>
</root>

174
Localization/Strings.resx Normal file
View File

@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="DownloadAndInstall" xml:space="preserve">
<value>Download &amp; Install</value>
</data>
<data name="InstallationFailed" xml:space="preserve">
<value>Installation failed</value>
</data>
<data name="InstallationSuccessfulOn" xml:space="preserve">
<value>Successfully installed on</value>
</data>
<data name="DownloadFailed" xml:space="preserve">
<value>Download failed:</value>
</data>
<data name="FailedLoadingReleases" xml:space="preserve">
<value>Failed to load releases:</value>
</data>
<data name="PleaseInstallTizen" xml:space="preserve">
<value>Tizen CLI is required but not found. Please install Tizen Studio first.</value>
</data>
<data name="DownloadingPackage" xml:space="preserve">
<value>Downloading package...</value>
</data>
<data name="ConnectingToDevice" xml:space="preserve">
<value>Connecting to device...</value>
</data>
<data name="RetrievingDeviceAddress" xml:space="preserve">
<value>Retrieving device address...</value>
</data>
<data name="TvNameNotFound" xml:space="preserve">
<value>TV Name could not be found...</value>
</data>
<data name="UpdatingCertificateProfile" xml:space="preserve">
<value>Updating certificate profile...</value>
</data>
<data name="PackagingWgtWithCertificate" xml:space="preserve">
<value>Packaging the wgt file with certificate...</value>
</data>
<data name="InstallationSuccessful" xml:space="preserve">
<value>Installation successful</value>
</data>
<data name="InstallingPackage" xml:space="preserve">
<value>Installing package on device...</value>
</data>
<data name="InstallationMaybeFailed" xml:space="preserve">
<value>Installation may have failed</value>
</data>
<data name="Output" xml:space="preserve">
<value>Output</value>
</data>
<data name="LoadingReleases" xml:space="preserve">
<value>Loading releases...</value>
</data>
<data name="ScanningNetwork" xml:space="preserve">
<value>Scanning network for Samsung TV...</value>
</data>
</root>

View File

@@ -4,8 +4,9 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Samsung_Jellyfin_Installer"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:viewmodels="clr-namespace:Samsung_Jellyfin_Installer.ViewModels"
xmlns:l = "clr-namespace:Samsung_Jellyfin_Installer.Localization"
TextElement.Foreground="{DynamicResource MaterialDesign.Brush.Foreground}"
Background="{DynamicResource MaterialDesign.Brush.Background}"
TextElement.FontWeight="Medium"
@@ -24,14 +25,15 @@
<Label Content="Version" HorizontalAlignment="Left" Margin="14,125,0,0" VerticalAlignment="Top" Width="85" Height="25"/>
<ComboBox Style="{DynamicResource MaterialDesignComboBox}" ItemsSource="{Binding AvailableAssets}" SelectedItem="{Binding SelectedAsset}" DisplayMemberPath="DisplayText" materialDesign:TextFieldAssist.UnderlineBrush="#FF000B25" Width="377" HorizontalAlignment="Left" Margin="95,125,0,0" VerticalAlignment="Top"/>
<Label Content="TV IP" HorizontalAlignment="Left" Margin="14,175,0,0" VerticalAlignment="Top" Width="85" Height="25"/>
<TextBox HorizontalAlignment="Left" Margin="95,175,0,0" TextWrapping="Wrap" Text="{Binding TvIpAddress}" VerticalAlignment="Top" Width="377"/>
<Label Content="Select TV" HorizontalAlignment="Left" Margin="14,175,0,0" VerticalAlignment="Top" Width="85" Height="25" />
<ComboBox Style="{DynamicResource MaterialDesignComboBox}" ItemsSource="{Binding AvailableDevices}" SelectedItem="{Binding SelectedDevice}" DisplayMemberPath="DisplayText" materialDesign:TextFieldAssist.UnderlineBrush="#FF000B25" Width="342" HorizontalAlignment="Left" Margin="95,175,0,0" VerticalAlignment="Top" IsEnabled="{Binding EnableDevicesInput}" />
<Button Command="{Binding RefreshDevicesCommand}" Style="{DynamicResource MaterialDesignFloatingActionLightButton}" Content="⟳" Height="25" Width="25" HorizontalAlignment="Left" Padding="0" Margin="447,175,0,0" VerticalAlignment="Top" IsEnabled="{Binding EnableDevicesInput}" Background="#FF000B25" BorderBrush="#FF000B25" Foreground="White" />
<materialDesign:Card Padding="12" Margin="0,215,0,0" Background="#FF000B25" Foreground="White" HorizontalAlignment="Center" Width="472" Height="38" VerticalAlignment="Top">
<TextBlock Text="{Binding StatusBar }" Style="{DynamicResource MaterialDesignTitleTextBlock}" HorizontalAlignment="Center"/>
</materialDesign:Card>
<Button Command="{Binding DownloadCommand}" CommandParameter="{Binding SelectedRelease}" Content="Download &amp; Installeer" HorizontalAlignment="Center" Margin="0,265,0,0" Style="{DynamicResource MaterialDesignRaisedLightButton}" Width="328" Background="#FF000B25" Foreground="White" Height="32" VerticalAlignment="Top"/>
<Button Command="{Binding DownloadCommand}" CommandParameter="{Binding SelectedRelease}" Content="{x:Static l:Strings.DownloadAndInstall}" HorizontalAlignment="Center" Margin="0,265,0,0" Style="{DynamicResource MaterialDesignRaisedLightButton}" Width="328" Background="#FF000B25" Foreground="White" Height="32" VerticalAlignment="Top"/>
<Label Content="Copyright (c) 2025 - MIT License - Patrick Stel" FontSize="12" HorizontalAlignment="Center" Margin="0,300,0,0" VerticalAlignment="Top" Width="480" HorizontalContentAlignment="Center"/>
</Grid>
</Window>
</Window>

26
Models/NetworkDevice.cs Normal file
View File

@@ -0,0 +1,26 @@
namespace Samsung_Jellyfin_Installer.Models;
public class NetworkDevice
{
public required string IpAddress { get; set; }
public string? Manufacturer { get; set; }
public string? DeviceName { get; set; }
public string DisplayText
{
get
{
if (DeviceName is not null)
{
return $"{IpAddress} ({DeviceName})";
}
if (Manufacturer is not null)
{
return $"{IpAddress} ({Manufacturer})";
}
return IpAddress;
}
}
}

View File

@@ -2,6 +2,8 @@
A simple tool to install Jellyfin on your Samsung Smart TV with ease.
Big shoutout to [jeppevinkel](https://github.com/jeppevinkel/jellyfin-tizen-builds) for sharing the Jellyfin Tizen .wgt files—super helpful and much appreciated!
## How It Works
Follow these steps to get Jellyfin up and running on your TV:
@@ -42,9 +44,12 @@ _Installation begins. Your TV will handle the rest!_
---
## Requirements
- Samsung Tizen TV (with developer mode enabled)
- The IP address of the TV
- Network connection between your computer and the TV
To use this tool, make sure you have the following:
- A **Samsung Tizen TV** with **Developer Mode enabled**
- The **IP address** of your TV
- A **network connection** between your computer and the TV (both must be on the same network)
- **Tizen Web CLI 5.5** — if not found in the usual locations, the tool will automatically download and install it for you
## Steps for Use

View File

@@ -32,4 +32,19 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Localization\Strings.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Strings.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Localization\Strings.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Strings.resx</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
using Samsung_Jellyfin_Installer.Models;
namespace Samsung_Jellyfin_Installer.Services;
public interface INetworkService
{
public Task<IEnumerable<NetworkDevice>> GetLocalTizenAddresses();
}

View File

@@ -9,5 +9,7 @@ namespace Samsung_Jellyfin_Installer.Services
Task<bool> EnsureTizenCliAvailable();
Task<string> DownloadPackageAsync(string downloadUrl);
Task<InstallResult> InstallPackageAsync(string packageUrl, string tvIpAddress, Action<string> updateStatus);
Task<string?> GetTvNameAsync(string tvIpAddress);
Task<bool> ConnectToTvAsync(string tvIpAddress);
}
}

177
Services/NetworkService.cs Normal file
View File

@@ -0,0 +1,177 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using Samsung_Jellyfin_Installer.Models;
namespace Samsung_Jellyfin_Installer.Services;
public class NetworkService : INetworkService
{
private readonly ITizenInstallerService _tizenInstaller;
private static readonly HttpClient _httpClient = new HttpClient();
private static readonly HashSet<string> _excludedInterfacePatterns = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"VirtualBox", "Loopback", "Docker", "Hyper-V",
"vEthernet", "VPN", "Bluetooth", "vSwitch"
};
public NetworkService(ITizenInstallerService tizenInstaller)
{
_tizenInstaller = tizenInstaller;
}
public async Task<IEnumerable<NetworkDevice>> GetLocalTizenAddresses()
{
return await FindTizenTvsAsync();
}
public async Task<IEnumerable<NetworkDevice>> FindTizenTvsAsync(CancellationToken cancellationToken = default)
{
const int tvPort = 26101;
const int scanTimeoutMs = 1000;
const int maxParallelScans = 100;
var foundDevices = new List<NetworkDevice>();
var localIps = GetRelevantLocalIPs();
var lockObject = new object();
await Task.WhenAll(localIps.SelectMany(localIp =>
{
var networkPrefix = GetNetworkPrefix(localIp);
return Enumerable.Range(1, 254)
.Select(i => $"{networkPrefix}.{i}")
.Select(async ip =>
{
try
{
using var cts = new CancellationTokenSource(scanTimeoutMs);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cts.Token, cancellationToken);
if (await IsPortOpenAsync(ip, tvPort, linkedCts.Token))
{
var manufacturer = await GetManufacturerFromIp(ip);
var device = new NetworkDevice
{
IpAddress = ip,
Manufacturer = manufacturer
};
lock (lockObject)
{
foundDevices.Add(device);
}
if (manufacturer?.Contains("Samsung", StringComparison.OrdinalIgnoreCase) == true)
{
device.DeviceName = await _tizenInstaller.GetTvNameAsync(ip);
}
}
}
catch { /* Ignore scan failures */ }
});
}));
Debug.WriteLine($"Scan complete! Found {foundDevices.Count} devices with port {tvPort} open.");
return foundDevices;
}
private IEnumerable<IPAddress> GetRelevantLocalIPs()
{
return NetworkInterface.GetAllNetworkInterfaces()
.Where(ni => ni.OperationalStatus == OperationalStatus.Up)
.Where(ni => !_excludedInterfacePatterns.Any(p =>
ni.Name.Contains(p, StringComparison.OrdinalIgnoreCase)))
.SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
.Where(ip => ip.Address.AddressFamily == AddressFamily.InterNetwork)
.Where(ip => !IPAddress.IsLoopback(ip.Address))
.Select(ip => ip.Address)
.Distinct();
}
private async Task<bool> IsPortOpenAsync(string ip, int port, CancellationToken ct)
{
try
{
using var client = new TcpClient();
var connectTask = client.ConnectAsync(ip, port, ct);
var timeoutTask = Task.Delay(Timeout.Infinite, ct);
var completedTask = await Task.WhenAny(connectTask.AsTask(), timeoutTask);
if (completedTask == connectTask.AsTask())
{
await connectTask; // Ensure connection succeeded
return true;
}
return false;
}
catch
{
return false;
}
}
private string GetNetworkPrefix(IPAddress ip)
{
var bytes = ip.GetAddressBytes();
return $"{bytes[0]}.{bytes[1]}.{bytes[2]}";
}
public static async Task<string?> GetManufacturerFromIp(string ipAddress)
{
string? macAddress = await GetMacAddressFromIp(ipAddress);
return string.IsNullOrEmpty(macAddress)
? null
: await GetManufacturerFromMac(macAddress);
}
private static async Task<string?> GetMacAddressFromIp(string ipAddress)
{
try
{
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "arp",
Arguments = $"-a {ipAddress}",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
}
};
process.Start();
string output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
var match = Regex.Match(output, @"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})");
return match.Success ? match.Value : null;
}
catch
{
return null;
}
}
private static async Task<string?> GetManufacturerFromMac(string macAddress)
{
try
{
string oui = macAddress
.Replace(":", "")
.Replace("-", "")
.Substring(0, 6)
.ToUpper();
return await _httpClient.GetStringAsync($"https://api.macvendors.com/{oui}");
}
catch
{
return null;
}
}
}

View File

@@ -1,12 +1,11 @@
using System.Diagnostics;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Xml.Linq;
using Samsung_Jellyfin_Installer.Localization;
namespace Samsung_Jellyfin_Installer.Services
{
@@ -18,15 +17,16 @@ namespace Samsung_Jellyfin_Installer.Services
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Tizen Studio"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "TizenStudio"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "TizenStudio"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "TizenStudioCli"),
"C:\\tizen-studio",
"C:\\Program Files\\TizenStudioCli",
Environment.GetEnvironmentVariable("TIZEN_STUDIO_HOME") ?? string.Empty
];
private readonly HttpClient _httpClient;
private readonly string _downloadDirectory;
public string TizenCliPath { get; private set; }
public string? TizenCliPath { get; private set; }
public string? TizenSdbPath { get; private set; }
public TizenInstallerService(HttpClient httpClient)
{
@@ -38,17 +38,69 @@ namespace Samsung_Jellyfin_Installer.Services
"Downloads");
Directory.CreateDirectory(_downloadDirectory);
TizenCliPath = FindTizenCliPath();
string? tizenRoot = FindTizenRoot();
if (tizenRoot is not null)
{
TizenCliPath = Path.Combine(tizenRoot, "tools", "ide", "bin", "tizen.bat");
TizenSdbPath = Path.Combine(tizenRoot, "tools", "sdb.exe");
}
}
public async Task<bool> EnsureTizenCliAvailable()
{
if (File.Exists(TizenCliPath))
if (File.Exists(TizenCliPath) && File.Exists(TizenSdbPath))
return true;
return await InstallMinimalCli();
}
public async Task<bool> ConnectToTvAsync(string tvIpAddress)
{
if (TizenSdbPath is null)
{
return false;
}
try
{
var result = await RunCommandAsync(TizenSdbPath, $"connect {tvIpAddress}");
return result.Contains($"connected to {tvIpAddress}");
}
catch (Exception)
{
return false;
}
}
public async Task<string?> GetTvNameAsync(string tvIpAddress)
{
if (TizenSdbPath is null)
{
return null;
}
try
{
await ConnectToTvAsync(tvIpAddress);
var output = await RunCommandAsync(TizenSdbPath, "devices");
var match = Regex.Match(output, @"(?<=\n)([^\s]+)\s+device\s+(?<name>[^\s]+)");
return match.Success ? match.Groups["name"].Value.Trim() : null;
}
catch (Exception ex)
{
Debug.WriteLine($"Failed to get TV name: {ex}");
}
finally
{
await RunCommandAsync(TizenSdbPath, $"disconnect {tvIpAddress}");
}
return null;
}
public async Task<string> DownloadPackageAsync(string downloadUrl)
{
var fileName = Path.GetFileName(new Uri(downloadUrl).LocalPath);
@@ -66,51 +118,64 @@ namespace Samsung_Jellyfin_Installer.Services
public async Task<InstallResult> InstallPackageAsync(string packageUrl, string tvIpAddress, Action<string> updateStatus)
{
if (TizenCliPath is null || TizenSdbPath is null)
{
updateStatus("Tizen Studio wasn't found");
return InstallResult.FailureResult("Tizen Studio wasn't found");
}
try
{
var studioRoot = Directory.GetParent(Directory.GetParent(Path.GetDirectoryName(TizenCliPath)).FullName).FullName;
var sdbPath = Path.Combine(studioRoot, "sdb.exe");
updateStatus(Strings.ConnectingToDevice);
await RunCommandAsync(TizenSdbPath, $"connect {tvIpAddress}");
updateStatus("Connecting to device...");
await RunCommandAsync(sdbPath, $"connect {tvIpAddress}");
updateStatus("Retrieving device adress...");
string tvName = await GetTvNameAsync(sdbPath);
updateStatus(Strings.RetrievingDeviceAddress);
string tvName = await GetTvNameAsync();
if (string.IsNullOrEmpty(tvName))
return InstallResult.FailureResult("TV Naam kon niet worden gevonden...");
return InstallResult.FailureResult(Strings.TvNameNotFound);
updateStatus("Updating certificate profile...");
updateStatus(Strings.UpdatingCertificateProfile);
UpdateProfileCertificatePaths();
updateStatus("Packaging the wgt file with certificate...");
await RunCommandAsync(TizenCliPath, $"package -t wgt -s custom -- {packageUrl}");
updateStatus(Strings.PackagingWgtWithCertificate);
updateStatus("Installing package on device...");
string installOutput = await RunCommandAsync(TizenCliPath, $"install -n {packageUrl} -t {tvName}");
await RunCommandAsync(TizenCliPath, $"package -t wgt -s custom -- \"{packageUrl}\"");
updateStatus(Strings.InstallingPackage);
string installOutput = await RunCommandAsync(TizenCliPath, $"install -n \"{packageUrl}\" -t {tvName}");
if (File.Exists(packageUrl) && !installOutput.Contains("Failed"))
{
updateStatus("Installation succesful");
updateStatus(Strings.InstallationSuccessful);
return InstallResult.SuccessResult();
}
updateStatus("Installation failed");
return InstallResult.FailureResult($"Installation may have failed. Output: {installOutput}");
updateStatus(Strings.InstallationFailed);
return InstallResult.FailureResult($"{Strings.InstallationMaybeFailed}. {Strings.Output}: {installOutput}");
}
catch (Exception ex)
{
updateStatus("Installation failed");
updateStatus(Strings.InstallationFailed);
return InstallResult.FailureResult(ex.Message);
}
finally
{
await RunCommandAsync(TizenSdbPath, $"disconnect {tvIpAddress}");
}
}
private static async Task<string> GetTvNameAsync(string sdbPath)
private async Task<string> GetTvNameAsync()
{
var output = await RunCommandAsync(sdbPath, "devices");
var match = Regex.Match(output, @"(?<=\n)(?<device>[^\s]+)\s+device");
if (TizenSdbPath is null)
{
return string.Empty;
}
var output = await RunCommandAsync(TizenSdbPath, "devices");
var match = Regex.Match(output, @"(?<=\n)([^\s]+)\s+device\s+(?<name>[^\s]+)");
return match.Success ? match.Groups["device"].Value.Trim() : "";
return match.Success ? match.Groups["name"].Value.Trim() : string.Empty;
}
private static void UpdateProfileCertificatePaths()
@@ -133,7 +198,8 @@ namespace Samsung_Jellyfin_Installer.Services
xml.Save(profilePath);
}
private static string FindTizenCliPath()
private static string? FindTizenRoot()
{
foreach (var basePath in PossibleTizenPaths)
{
@@ -141,10 +207,12 @@ namespace Samsung_Jellyfin_Installer.Services
var possiblePath = Path.Combine(basePath, "tools", "ide", "bin", "tizen.bat");
if (File.Exists(possiblePath))
return possiblePath;
return basePath;
}
return null;
}
private static async Task<string> RunCommandAsync(string fileName, string arguments)
{
var psi = new ProcessStartInfo
@@ -155,6 +223,7 @@ namespace Samsung_Jellyfin_Installer.Services
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WorkingDirectory = Path.GetDirectoryName(fileName)
};
using var proc = new Process
@@ -207,7 +276,11 @@ namespace Samsung_Jellyfin_Installer.Services
{
const string installerUrl = "https://download.tizen.org/sdk/Installer/tizen-studio_5.5/web-cli_Tizen_Studio_5.5_windows-64.exe";
installerPath = await DownloadPackageAsync(installerUrl);
string installPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "TizenStudioCli");
string installPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Programs",
"TizenStudioCli"
);
var startInfo = new ProcessStartInfo
{
@@ -220,12 +293,12 @@ namespace Samsung_Jellyfin_Installer.Services
using var process = Process.Start(startInfo);
await process.WaitForExitAsync();
MessageBox.Show(process.ExitCode.ToString());
if (process.ExitCode == 0)
{
TizenCliPath = FindTizenCliPath();
return TizenCliPath != null;
var tizenRoot = FindTizenRoot() ?? string.Empty;
TizenCliPath = Path.Combine(tizenRoot, "tools", "ide", "bin", "tizen.bat");
TizenSdbPath = Path.Combine(tizenRoot, "tools", "sdb.exe");
return tizenRoot != string.Empty;
}
return false;
}

View File

@@ -8,6 +8,7 @@ using Samsung_Jellyfin_Installer.Services;
using System.Diagnostics;
using System.IO;
using System.Windows;
using Samsung_Jellyfin_Installer.Localization;
namespace Samsung_Jellyfin_Installer.ViewModels
{
@@ -16,13 +17,15 @@ namespace Samsung_Jellyfin_Installer.ViewModels
private readonly ITizenInstallerService _tizenInstaller;
private readonly IDialogService _dialogService;
private readonly HttpClient _httpClient;
private readonly INetworkService _networkService;
private ObservableCollection<GitHubRelease> _releases = new ObservableCollection<GitHubRelease>();
private GitHubRelease _selectedRelease;
private bool _isLoading;
private bool _isLoading, _isLoadingDevices;
private ObservableCollection<Asset> _availableAssets = new ObservableCollection<Asset>();
private Asset _selectedAsset;
private string _tvIpAddress;
private ObservableCollection<NetworkDevice> _availableDevices = new ObservableCollection<NetworkDevice>();
private NetworkDevice? _selectedDevice;
private string _statusBar;
public ObservableCollection<GitHubRelease> Releases
@@ -58,6 +61,18 @@ namespace Samsung_Jellyfin_Installer.ViewModels
set => SetField(ref _selectedAsset, value);
}
public ObservableCollection<NetworkDevice> AvailableDevices
{
get => _availableDevices;
private set => SetField(ref _availableDevices, value);
}
public NetworkDevice? SelectedDevice
{
get => _selectedDevice;
set => SetField(ref _selectedDevice, value);
}
public bool IsLoading
{
get => _isLoading;
@@ -70,12 +85,21 @@ namespace Samsung_Jellyfin_Installer.ViewModels
}
}
public string TvIpAddress
public bool IsLoadingDevices
{
get => _tvIpAddress;
set => SetField(ref _tvIpAddress, value);
get => _isLoadingDevices;
private set
{
if (SetField(ref _isLoadingDevices, value))
{
OnPropertyChanged(nameof(EnableDevicesInput));
CommandManager.InvalidateRequerySuggested();
}
}
}
public bool EnableDevicesInput => !IsLoadingDevices;
public string StatusBar
{
get => _statusBar;
@@ -83,18 +107,22 @@ namespace Samsung_Jellyfin_Installer.ViewModels
}
public ICommand RefreshCommand { get; }
public ICommand RefreshDevicesCommand { get; }
public ICommand DownloadCommand { get; }
public MainWindowViewModel(
ITizenInstallerService tizenInstaller,
IDialogService dialogService,
HttpClient httpClient)
HttpClient httpClient,
INetworkService networkService)
{
_tizenInstaller = tizenInstaller;
_dialogService = dialogService;
_httpClient = httpClient;
_networkService = networkService;
RefreshCommand = new RelayCommand(async () => await LoadReleasesAsync());
RefreshDevicesCommand = new RelayCommand(async () => await LoadDevicesAsync());
DownloadCommand = new RelayCommand<GitHubRelease>(async r => await DownloadReleaseAsync(r));
InitializeAsync();
@@ -105,9 +133,13 @@ namespace Samsung_Jellyfin_Installer.ViewModels
if (!await _tizenInstaller.EnsureTizenCliAvailable())
{
await _dialogService.ShowErrorAsync(
"Tizen CLI is required but not found. Please install Tizen Studio first.");
Strings.PleaseInstallTizen);
}
StatusBar = $"{Strings.InstallationFailed}";
await LoadReleasesAsync();
StatusBar = $"{Strings.ScanningNetwork}";
await LoadDevicesAsync();
StatusBar = $"{Strings.DownloadAndInstall}";
}
private async Task DownloadReleaseAsync(GitHubRelease release)
@@ -118,26 +150,26 @@ namespace Samsung_Jellyfin_Installer.ViewModels
string downloadPath = null;
try
{
StatusBar = "Downloading package...";
StatusBar = Strings.DownloadingPackage;
downloadPath = await _tizenInstaller.DownloadPackageAsync(SelectedAsset.DownloadUrl);
// Automatically trigger installation if TV IP is set
if (!string.IsNullOrWhiteSpace(TvIpAddress))
if (!string.IsNullOrWhiteSpace(SelectedDevice?.IpAddress))
{
var result = await _tizenInstaller.InstallPackageAsync(
downloadPath,
TvIpAddress,
SelectedDevice.IpAddress,
status => Application.Current.Dispatcher.Invoke(() => StatusBar = status));
if (result.Success)
{
await _dialogService.ShowMessageAsync(
$"Successfully installed on {TvIpAddress}");
$"{Strings.InstallationSuccessfulOn} {SelectedDevice.IpAddress}");
}
else
{
await _dialogService.ShowErrorAsync(
$"Installation failed: {result.ErrorMessage}");
$"{Strings.InstallationFailed}: {result.ErrorMessage}");
}
}
}
@@ -145,7 +177,7 @@ namespace Samsung_Jellyfin_Installer.ViewModels
{
Debug.WriteLine($"Download failed: {ex.Message}");
await _dialogService.ShowErrorAsync(
$"Download failed: {ex.Message}");
$"{Strings.DownloadFailed} {ex.Message}");
}
finally
{
@@ -188,12 +220,50 @@ namespace Samsung_Jellyfin_Installer.ViewModels
{
Debug.WriteLine($"Release load error: {ex.Message}");
await _dialogService.ShowErrorAsync(
$"Failed to load releases: {ex.Message}");
$"{Strings.FailedLoadingReleases} {ex.Message}");
}
finally
{
IsLoading = false;
}
}
private async Task LoadDevicesAsync()
{
IsLoadingDevices = true;
AvailableDevices.Clear();
try
{
string? selectedIp = null;
if (SelectedDevice is not null)
{
selectedIp = SelectedDevice.IpAddress;
}
var devices = await _networkService.GetLocalTizenAddresses();
foreach (NetworkDevice device in devices)
{
AvailableDevices.Add(device);
}
SelectedDevice = AvailableDevices.Count switch
{
> 0 when SelectedDevice is null => AvailableDevices[0],
> 0 when selectedIp is not null =>
AvailableDevices.FirstOrDefault(it => it.IpAddress == selectedIp),
_ => SelectedDevice
};
}
catch (Exception ex)
{
Debug.WriteLine($"Devices load error: {ex.Message}");
await _dialogService.ShowErrorAsync(
$"Failed to load devices: {ex.Message}");
}
finally
{
IsLoadingDevices = false;
}
}
}
}

View File

@@ -16,7 +16,7 @@
Remove this element if your application requires this virtualization for backwards
compatibility.
-->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>