From fe85d2dcc435f14b75e866179af4d471a9065cc2 Mon Sep 17 00:00:00 2001 From: azures04 Date: Mon, 9 Feb 2026 04:24:33 +0100 Subject: [PATCH] Big commit --- ...Boilerplate.csproj => BrikInstaller.csproj | 6 + Photino-Boilerplate.sln => BrikInstaller.sln | 2 +- src/main/Constants.cs | 19 ++ src/main/Program.cs | 118 ++++++-- src/main/helpers/Bash.cs | 70 +++++ src/main/helpers/Http.cs | 51 ++++ src/main/helpers/Os.cs | 31 ++ src/main/helpers/Window.cs | 51 +++- src/main/schemas/GenericBrikFiles.cs | 20 ++ src/main/schemas/Product.cs | 10 + src/main/schemas/Requirement.cs | 9 + src/main/services/Files.cs | 56 ++++ src/main/services/IPC.cs | 19 ++ src/main/services/Installation.cs | 76 +++++ src/main/services/Network.cs | 30 ++ src/main/services/Requirements.cs | 96 ++++++ src/resources/build/icon.ico | Bin 0 -> 110640 bytes src/resources/build/logo.png | Bin 0 -> 1253 bytes src/resources/renderer/assets/css/common.css | 166 ++++++++++ src/resources/renderer/assets/css/index.css | 166 ++++++++-- src/resources/renderer/assets/css/select.css | 76 +++++ src/resources/renderer/assets/img/logo.png | Bin 0 -> 1253 bytes src/resources/renderer/assets/js/common.js | 43 +++ src/resources/renderer/assets/js/frames.js | 32 ++ src/resources/renderer/assets/js/index.js | 278 ++++++++++++++++- src/resources/renderer/assets/js/ipc.js | 39 ++- src/resources/renderer/assets/js/select.js | 26 ++ .../renderer/assets/locales/arab.json | 26 ++ src/resources/renderer/assets/locales/dz.json | 26 ++ src/resources/renderer/assets/locales/en.json | 26 ++ src/resources/renderer/assets/locales/es.json | 26 ++ src/resources/renderer/assets/locales/et.json | 26 ++ src/resources/renderer/assets/locales/fr.json | 26 ++ src/resources/renderer/assets/locales/it.json | 26 ++ src/resources/renderer/assets/locales/jp.json | 26 ++ src/resources/renderer/assets/locales/ma.json | 26 ++ src/resources/renderer/assets/locales/ng.json | 26 ++ src/resources/renderer/assets/locales/ru.json | 26 ++ src/resources/renderer/assets/locales/sn.json | 26 ++ .../renderer/assets/locales/tamazight.json | 26 ++ src/resources/renderer/assets/locales/tn.json | 26 ++ src/resources/renderer/assets/locales/tz.json | 26 ++ src/resources/renderer/assets/locales/za.json | 26 ++ src/resources/renderer/index.html | 285 +++++++++++++++++- 44 files changed, 2120 insertions(+), 71 deletions(-) rename Photino-Boilerplate.csproj => BrikInstaller.csproj (85%) rename Photino-Boilerplate.sln => BrikInstaller.sln (86%) create mode 100644 src/main/Constants.cs create mode 100644 src/main/helpers/Bash.cs create mode 100644 src/main/helpers/Http.cs create mode 100644 src/main/helpers/Os.cs create mode 100644 src/main/schemas/GenericBrikFiles.cs create mode 100644 src/main/schemas/Product.cs create mode 100644 src/main/schemas/Requirement.cs create mode 100644 src/main/services/Files.cs create mode 100644 src/main/services/IPC.cs create mode 100644 src/main/services/Installation.cs create mode 100644 src/main/services/Network.cs create mode 100644 src/main/services/Requirements.cs create mode 100644 src/resources/build/icon.ico create mode 100644 src/resources/build/logo.png create mode 100644 src/resources/renderer/assets/css/common.css create mode 100644 src/resources/renderer/assets/css/select.css create mode 100644 src/resources/renderer/assets/img/logo.png create mode 100644 src/resources/renderer/assets/js/common.js create mode 100644 src/resources/renderer/assets/js/frames.js create mode 100644 src/resources/renderer/assets/js/select.js create mode 100644 src/resources/renderer/assets/locales/arab.json create mode 100644 src/resources/renderer/assets/locales/dz.json create mode 100644 src/resources/renderer/assets/locales/en.json create mode 100644 src/resources/renderer/assets/locales/es.json create mode 100644 src/resources/renderer/assets/locales/et.json create mode 100644 src/resources/renderer/assets/locales/fr.json create mode 100644 src/resources/renderer/assets/locales/it.json create mode 100644 src/resources/renderer/assets/locales/jp.json create mode 100644 src/resources/renderer/assets/locales/ma.json create mode 100644 src/resources/renderer/assets/locales/ng.json create mode 100644 src/resources/renderer/assets/locales/ru.json create mode 100644 src/resources/renderer/assets/locales/sn.json create mode 100644 src/resources/renderer/assets/locales/tamazight.json create mode 100644 src/resources/renderer/assets/locales/tn.json create mode 100644 src/resources/renderer/assets/locales/tz.json create mode 100644 src/resources/renderer/assets/locales/za.json diff --git a/Photino-Boilerplate.csproj b/BrikInstaller.csproj similarity index 85% rename from Photino-Boilerplate.csproj rename to BrikInstaller.csproj index 2b7ed89..f38a49c 100644 --- a/Photino-Boilerplate.csproj +++ b/BrikInstaller.csproj @@ -7,6 +7,7 @@ enable enable false + src/resources/build/icon.ico @@ -18,6 +19,11 @@ + + + PreserveNewest + + diff --git a/Photino-Boilerplate.sln b/BrikInstaller.sln similarity index 86% rename from Photino-Boilerplate.sln rename to BrikInstaller.sln index b092ffb..b67ed25 100644 --- a/Photino-Boilerplate.sln +++ b/BrikInstaller.sln @@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.2.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Photino-Boilerplate", "Photino-Boilerplate.csproj", "{5FF73963-D009-62ED-7350-FF17EC625A26}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BrikInstaller", "BrikInstaller.csproj", "{5FF73963-D009-62ED-7350-FF17EC625A26}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/main/Constants.cs b/src/main/Constants.cs new file mode 100644 index 0000000..6f490d6 --- /dev/null +++ b/src/main/Constants.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Text.Json; +using BrikInstaller.Utils; + +namespace BrikInstaller; + +public static class Constants { + public static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + public static readonly string os = OsHelper.getOperatingSystem(); + public static readonly string arch = OsHelper.getArchitecture(); + + public static readonly string env = "DEV"; + public static readonly string __dirname = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + public static readonly string BaseDirectory = AppDomain.CurrentDomain.BaseDirectory; + + public static class URLs { + public static readonly string basePoint = "https://brik.azures.fr/products"; + } +} \ No newline at end of file diff --git a/src/main/Program.cs b/src/main/Program.cs index 2f6371f..8a27a43 100644 --- a/src/main/Program.cs +++ b/src/main/Program.cs @@ -1,28 +1,29 @@ -using System.Reflection; -using System.Text.Json; +using Photino.NET; using BrikPackager; -using PhotinoBoilerplate.Utils; -using Photino.NET; +using System.Text.Json; +using BrikInstaller.Utils; +using BrikInstaller.Schemas; +using BrikInstaller.Services; -namespace PhotinoBoilerplate; +namespace BrikInstaller; class Program{ - - public static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - private static readonly string __dirname = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; - [STAThread] static void Main(string[] args) { CreateMainWindow().WaitForClose(); } public static PhotinoWindow CreateMainWindow() { - BrikPackage bpkg = new BrikPackage(Path.Combine(__dirname, "root.dat"), 66); + BrikPackage bpkg = new BrikPackage(Path.Combine(Constants.__dirname, "root.dat"), 66); List entries = bpkg.ListEntries(); + foreach (string entry in entries) { + Console.WriteLine(entry); + } var window = new PhotinoWindow() - .SetTitle("Photino Boilerplate") + .SetTitle("Brik Installer") .SetUseOsDefaultLocation(false) .SetUseOsDefaultSize(false) + .SetChromeless(true) .RegisterCustomSchemeHandler("http", (object sender, string scheme, string url, out string contentType) =>{ Uri uri = new Uri(url); switch (uri.Host){ @@ -38,41 +39,112 @@ class Program{ }); window.WindowCreated += (sender, e) => { + string iconPath = Path.Combine(Constants.BaseDirectory, "src", "resources", "build", "icon.ico"); SetupBridge(window); - WindowHelper.setSize(window, 900, 600); + WindowHelper.setSize(window, 465, 290); + WindowHelper.RemoveTitleBar(window); + WindowHelper.ForceTaskbarIcon(window, iconPath); window.Center(); }; - - window.Load("http://internal/index.html"); - + LoadPage(window, "index.html"); return window; } - public static void SetupBridge(PhotinoWindow window) { + public static void SetupBridge(PhotinoWindow window) { window.RegisterWebMessageReceivedHandler(async (object? sender, string message) => { try { var json = JsonDocument.Parse(message); string requestId = json.RootElement.GetProperty("requestId").GetString()!; string method = json.RootElement.GetProperty("functionName").GetString()!; var jsonPayload = json.RootElement.GetProperty("payload"); + + object? responsePayload = null; var pw = (PhotinoWindow)sender!; - object? payload = null; - switch (method) { - case "hello::world": - payload = "Hello from C#"; + case "network::fetch": + responsePayload = await NetworkService.HandleFetchRequest(jsonPayload); + break; + case "check::requirements": + if (jsonPayload.TryGetProperty("soft", out var soft)) { + RequirementCheckingResponse requirementChecking = await RequirementsService.CheckRequirementsAsync(pw, soft.GetString()!); + if (!requirementChecking.Success) { + IPC.sendToRenderer(window, "requirement::failed", null!); + } else { + IPC.sendToRenderer(window, "requirement::success", null!); + } + } else { + responsePayload = new { success = false, error = "Soft name missing, wtf?" }; + } + break; + case "browser::open": + if (jsonPayload.TryGetProperty("url", out var url)) { + BashUtils.OpenUrl(url.ToString()); + } else { + responsePayload = new { success = false, error = "Soft name missing, wtf?" }; + } + break; + case "installer::elevate": + InstallationService.ElevateAndRestart(); + responsePayload = new { success = true }; + break; + case "installer::testInstallationPath": + if (jsonPayload.TryGetProperty("path", out var installPath)) { + bool canWrite = InstallationService.CanCreateInPath(installPath.ToString()); + if (!canWrite) { + InstallationService.ElevateAndRestart(); + responsePayload = new { success = canWrite }; + } else { + responsePayload = new { success = canWrite }; + } + } else { + responsePayload = new { success = false }; + } + break; + case "dialog::selectDirectory": + string[] selectedFolders = window.ShowOpenFolder(); + + if (selectedFolders != null && selectedFolders.Length > 0) { + responsePayload = new { success = true, path = selectedFolders[0] }; + } else { + responsePayload = new { success = false }; + } + break; + case "installer::defaultInstallPath": + if (jsonPayload.TryGetProperty("soft", out var productName)) { + string defaultInstallPath = InstallationService.GetDefaultInstallationPath(productName.ToString()); + responsePayload = new { success = true, path = defaultInstallPath }; + } else { + responsePayload = new { success = false }; + } + break; + case "installer::install": + if (jsonPayload.TryGetProperty("soft", out var softName) && jsonPayload.TryGetProperty("path", out var softInstallPath)) { + try { + await GenericFilesService.SyncFilesAsync($"{Constants.URLs.basePoint}/{softName}/{Constants.os}-{Constants.arch}/download/", $"{Constants.URLs.basePoint}/{softName}/{Constants.os}-{Constants.arch}/manifest", softInstallPath.ToString()); + responsePayload = new { success = true }; + } catch (Exception) { + responsePayload = new { success = false }; + } + } break; - } - var response = new { requestId, payload }; - window.SendWebMessage(JsonSerializer.Serialize(response, _jsonOptions)); + var finalResponse = new { requestId, payload = responsePayload }; + window.SendWebMessage(JsonSerializer.Serialize(finalResponse, Constants._jsonOptions)); } catch (Exception ex) { Console.WriteLine($"Error stacktrace: {ex.StackTrace}"); Console.WriteLine($"Bridge error: {ex.Message}"); } }); } + + public static void LoadPage(PhotinoWindow window, string url) { + if (Constants.env == "PROD") { + window.Load($"http://internal/{url}"); + } else { + window.Load($"src/resources/renderer/{url}"); + } + } } \ No newline at end of file diff --git a/src/main/helpers/Bash.cs b/src/main/helpers/Bash.cs new file mode 100644 index 0000000..b81bd7d --- /dev/null +++ b/src/main/helpers/Bash.cs @@ -0,0 +1,70 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace BrikInstaller.Utils; + +public static class BashUtils { + public static string GetCommandOutput(string command) { + string shell = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : "/bin/bash"; + string argPrefix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "/c" : "-c"; + + string formattedCommand = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? command + : $"\"{command.Replace("\"", "\\\"")}\""; + + using var proc = new Process { + StartInfo = new ProcessStartInfo { + FileName = shell, + Arguments = $"{argPrefix} {formattedCommand}", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + StandardOutputEncoding = System.Text.Encoding.UTF8 + } + }; + + try { + proc.Start(); + string output = proc.StandardOutput.ReadToEnd(); + string error = proc.StandardError.ReadToEnd(); + proc.WaitForExit(); + + return string.IsNullOrEmpty(output) ? error : output; + } catch (Exception ex) { + return $"Execution error : {ex.Message}"; + } + } + + public static string GetBashCommandOutput(string command) { + using var proc = new Process { + StartInfo = new ProcessStartInfo { + FileName = "/bin/bash", + Arguments = $"-c \"{command}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + } + }; + proc.Start(); + return proc.StandardOutput.ReadToEnd(); + } + + public static void OpenUrl(string url) { + try { + Process.Start(url); + } + catch { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + url = url.Replace("&", "^&"); + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + Process.Start("xdg-open", url); + } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + Process.Start("open", url); + } else { + throw; + } + } + } +} \ No newline at end of file diff --git a/src/main/helpers/Http.cs b/src/main/helpers/Http.cs new file mode 100644 index 0000000..d783549 --- /dev/null +++ b/src/main/helpers/Http.cs @@ -0,0 +1,51 @@ +using System.Net.Http.Json; +using BrikInstaller; + +public static class HttpHelper { + private static readonly HttpClient Http = new HttpClient(); + + public static async Task FetchAsync(string url, HttpMethod? method = null, object? body = null, Dictionary? headers = null) { + Console.WriteLine($"Fetching {url} using {method} method"); + var request = new HttpRequestMessage(method ?? HttpMethod.Get, url); + + if (headers != null) { + foreach (var header in headers) { + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } + + if (body != null) { + if (body is HttpContent httpContent) { + request.Content = httpContent; + } else if (body is string s) { + request.Content = new StringContent(s, System.Text.Encoding.UTF8, "application/json"); + } else { + request.Content = JsonContent.Create(body, options: Constants._jsonOptions); + } + } + + request.Headers.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + + return await Http.SendAsync(request); + } + + public static async Task DownloadFileAsync(string url, string destinationPath) { + Console.WriteLine($"Downloading file from {url} to {destinationPath}"); + string? directory = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(directory)) { + Directory.CreateDirectory(directory); + } + + if (File.Exists(destinationPath)) { + File.Delete(destinationPath); + } + + using var response = await HttpHelper.FetchAsync(url); + response.EnsureSuccessStatusCode(); + + using var streamToReadFrom = await response.Content.ReadAsStreamAsync(); + using var streamToWriteTo = File.Create(destinationPath); + + await streamToReadFrom.CopyToAsync(streamToWriteTo); + } +} \ No newline at end of file diff --git a/src/main/helpers/Os.cs b/src/main/helpers/Os.cs new file mode 100644 index 0000000..fa38fbd --- /dev/null +++ b/src/main/helpers/Os.cs @@ -0,0 +1,31 @@ +using System.Runtime.InteropServices; + +namespace BrikInstaller.Utils; + +public static class OsHelper { + + public static string getOperatingSystem() { + string os = "unknown"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + os = "win"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + os = "linux"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + os = "macos"; + } + return os; + } + + public static string getArchitecture() { + string arch = RuntimeInformation.OSArchitecture switch { + Architecture.X64 => "x64", + Architecture.X86 => "x86", + Architecture.Arm64 => "arm64", + Architecture.Arm => "arm", + _ => RuntimeInformation.OSArchitecture.ToString().ToLower() + }; + return arch; + } +} \ No newline at end of file diff --git a/src/main/helpers/Window.cs b/src/main/helpers/Window.cs index 91ed8cb..9843b05 100644 --- a/src/main/helpers/Window.cs +++ b/src/main/helpers/Window.cs @@ -1,13 +1,39 @@ using Photino.NET; using System.Drawing; +using System.Runtime.InteropServices; -namespace PhotinoBoilerplate.Utils; +namespace BrikInstaller.Utils; public static class WindowHelper { private const float StandardDpi = 96.0f; + + [DllImport("user32.dll")] + static extern int GetWindowLong(IntPtr hWnd, int nIndex); + [DllImport("user32.dll")] + static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); + [DllImport("user32.dll")] + static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + [DllImport("user32.dll", SetLastError = true)] + static extern IntPtr LoadImage(IntPtr hinst, string lpszName, uint uType, int cxDesired, int cyDesired, uint fuLoad); - + const int GWL_STYLE = -16; + const int WS_CAPTION = 0x00C00000; + const int WS_THICKFRAME = 0x00040000; + const int WS_MINIMIZEBOX = 0x00020000; + const int WS_MAXIMIZEBOX = 0x00010000; + const int WS_SYSMENU = 0x00080000; + const uint SWP_FRAMECHANGED = 0x0020; + const uint SWP_NOMOVE = 0x0002; + const uint SWP_NOSIZE = 0x0001; + const uint SWP_NOZORDER = 0x0004; + const uint WM_SETICON = 0x0080; + const IntPtr ICON_SMALL = 0; + const IntPtr ICON_BIG = 1; + const uint IMAGE_ICON = 1; + const uint LR_LOADFROMFILE = 0x00000010; public static Size GetScaledSize(PhotinoWindow window, int logicalWidth, int logicalHeight) { float currentDpi = window.ScreenDpi > 0 ? window.ScreenDpi : StandardDpi; @@ -22,8 +48,27 @@ public static class WindowHelper { public static void setSize(PhotinoWindow window, int width, int height) { Size targetSize = GetScaledSize(window, width, height); window - .SetResizable(true) + .SetResizable(false) .SetSize(targetSize) .SetMinSize(targetSize.Width, targetSize.Height); } + + public static void RemoveTitleBar(PhotinoWindow window) { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + IntPtr hWnd = window.WindowHandle; + int style = GetWindowLong(hWnd, GWL_STYLE); + style &= ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU); + SetWindowLong(hWnd, GWL_STYLE, style); + SetWindowPos(hWnd, IntPtr.Zero, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER); + } + } + + public static void ForceTaskbarIcon(PhotinoWindow window, string iconPath) { + IntPtr hWnd = window.WindowHandle; + IntPtr hIcon = LoadImage(IntPtr.Zero, iconPath, IMAGE_ICON, 0, 0, LR_LOADFROMFILE); + if (hIcon != IntPtr.Zero) { + SendMessage(hWnd, WM_SETICON, ICON_SMALL, hIcon); + SendMessage(hWnd, WM_SETICON, ICON_BIG, hIcon); + } + } } \ No newline at end of file diff --git a/src/main/schemas/GenericBrikFiles.cs b/src/main/schemas/GenericBrikFiles.cs new file mode 100644 index 0000000..4a00f7f --- /dev/null +++ b/src/main/schemas/GenericBrikFiles.cs @@ -0,0 +1,20 @@ +namespace BrikInstaller.Schemas; + +public class GenericArtifact { + public string Path { get; set; } = string.Empty; + public string Sha1 { get; set; } = string.Empty; + public long Size { get; set; } + public string Url { get; set; } = string.Empty; +} + +public class BrikFilesIndex { + public List Root { get; set; } = new(); +} + +public class ArtifactEntry { + public ArtifactContainer Downloads { get; set; } = null!; +} + +public class ArtifactContainer { + public GenericArtifact Artifact { get; set; } = null!; +} \ No newline at end of file diff --git a/src/main/schemas/Product.cs b/src/main/schemas/Product.cs new file mode 100644 index 0000000..35262b4 --- /dev/null +++ b/src/main/schemas/Product.cs @@ -0,0 +1,10 @@ +namespace BrikInstaller.Schemas; + +public class ProductRequirement { + public string? Id { get; set; } + public string? Name { get; set; } + public string? CheckCommand { get; set; } + public string? ExpectedOutputPattern { get; set; } + public string? InstallationMode { get; set; } + public string? InstallationUrl { get; set; } +} \ No newline at end of file diff --git a/src/main/schemas/Requirement.cs b/src/main/schemas/Requirement.cs new file mode 100644 index 0000000..5f71628 --- /dev/null +++ b/src/main/schemas/Requirement.cs @@ -0,0 +1,9 @@ +namespace BrikInstaller.Schemas; + +public class RequirementCheckingResponse { + public bool Success { get; set; } + public string? Error { get; set; } + public int? Status { get; set; } + + public bool? hasNoDeps { get; set; } +} \ No newline at end of file diff --git a/src/main/services/Files.cs b/src/main/services/Files.cs new file mode 100644 index 0000000..b7341d8 --- /dev/null +++ b/src/main/services/Files.cs @@ -0,0 +1,56 @@ +using System.Security.Cryptography; +using System.Net.Http.Json; +using BrikInstaller.Schemas; + +namespace BrikInstaller.Services; + +public static class GenericFilesService { + private static readonly HttpClient _httpClient = new(); + + public static async Task SyncFilesAsync(string rootEndpoint, string apiUrl, string destinationDir) { + try { + var index = await _httpClient.GetFromJsonAsync(apiUrl); + if (index?.Root == null) return; + + var artifacts = index.Root.Select(r => r.Downloads.Artifact).ToList(); + await ProcessDownloadsAsync(rootEndpoint, artifacts, destinationDir); + } + catch (Exception ex) { + Console.WriteLine($"[Sync Error] {ex.Message}"); + } + } + + private static async Task ProcessDownloadsAsync(string rootEndpoint, List artifacts, string baseDir) { + var queue = artifacts.Where(a => !IsFileValid(Path.Combine(baseDir, a.Path), a)).ToList(); + + if (queue.Count == 0) return; + + await Parallel.ForEachAsync(queue, new ParallelOptions { MaxDegreeOfParallelism = 4 }, async (artifact, token) => { + string localPath = Path.Combine(baseDir, artifact.Path); + + try { + await HttpHelper.DownloadFileAsync(rootEndpoint + artifact.Url, localPath); + + if (!IsFileValid(localPath, artifact)) { + Console.WriteLine($"[Validation Failed] {artifact.Path}"); + } + } + catch (Exception ex) { + Console.WriteLine($"[Download Error] {artifact.Path}: {ex.Message}"); + } + }); + } + + public static bool IsFileValid(string filePath, GenericArtifact expected) { + if (!File.Exists(filePath)) return false; + + var info = new FileInfo(filePath); + if (info.Length != expected.Size) return false; + + using var stream = File.OpenRead(filePath); + byte[] hashBytes = SHA1.HashData(stream); + string hash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + + return hash == expected.Sha1.ToLowerInvariant(); + } +} \ No newline at end of file diff --git a/src/main/services/IPC.cs b/src/main/services/IPC.cs new file mode 100644 index 0000000..a5ae4fb --- /dev/null +++ b/src/main/services/IPC.cs @@ -0,0 +1,19 @@ +using System.Text.Json; +using Photino.NET; + +namespace BrikInstaller.Services; + +public static class IPC { + + public static void sendRequirementCheckConsoleLog(PhotinoWindow window, string log) { + sendToRenderer(window, "requirement::log", log); + } + + public static void sendToRenderer(PhotinoWindow window, string requestId, object data) { + var logMessage = new { + requestId, + payload = new { message = data } + }; + window.SendWebMessage(JsonSerializer.Serialize(logMessage, Constants._jsonOptions)); + } +} \ No newline at end of file diff --git a/src/main/services/Installation.cs b/src/main/services/Installation.cs new file mode 100644 index 0000000..92c3065 --- /dev/null +++ b/src/main/services/Installation.cs @@ -0,0 +1,76 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace BrikInstaller.Services; + +public static class InstallationService { + public static bool CanCreateInPath(string folderPath) { + if (string.IsNullOrWhiteSpace(folderPath)) return false; + + try { + string fullPath = Path.GetFullPath(folderPath); + DirectoryInfo di = new DirectoryInfo(fullPath); + + while (di != null && !di.Exists) { + di = di.Parent!; + } + + if (di == null) return false; + + string testPath = Path.Combine(di.FullName, Guid.NewGuid().ToString()); + + Directory.CreateDirectory(testPath); + Directory.Delete(testPath); + + return true; + } catch (UnauthorizedAccessException) { + return false; + } catch (Exception) { + return false; + } + } + + public static void ElevateAndRestart() { + string exePath = Process.GetCurrentProcess().MainModule!.FileName; + ProcessStartInfo startInfo = new ProcessStartInfo(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + startInfo.FileName = exePath; + startInfo.Verb = "runas"; + startInfo.UseShellExecute = true; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + startInfo.FileName = "pkexec"; + startInfo.Arguments = $"\"{exePath}\""; + startInfo.UseShellExecute = false; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + startInfo.FileName = "osascript"; + startInfo.Arguments = $"-e \"do shell script \\\"{exePath}\\\" with administrator privileges\""; + startInfo.UseShellExecute = false; + } + + try { + Process.Start(startInfo); + Environment.Exit(0); + } catch (Exception ex) { + Console.WriteLine($"Elevation failed : {ex.Message}"); + } + } + + public static string GetDefaultInstallationPath(string appName) { + string final = ""; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + final = Path.Combine(localAppData, "Programs", appName); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + final = Path.Combine(home, ".local", "share", appName.ToLower().Replace(" ", "_")); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + final = $"/Applications/{appName}.app"; + } + return final; + } +} \ No newline at end of file diff --git a/src/main/services/Network.cs b/src/main/services/Network.cs new file mode 100644 index 0000000..1b93996 --- /dev/null +++ b/src/main/services/Network.cs @@ -0,0 +1,30 @@ +using System.Text.Json; +using BrikInstaller; + +public static class NetworkService { + public static async Task HandleFetchRequest(JsonElement jsonPayload) { + try { + string url = jsonPayload.GetProperty("url").GetString()!; + string httpMethodStr = jsonPayload.TryGetProperty("method", out var m) ? m.GetString()! : "GET"; + string? body = jsonPayload.TryGetProperty("body", out var b) ? b.GetString() : null; + + Dictionary? headers = null; + if (jsonPayload.TryGetProperty("headers", out var h)) { + headers = JsonSerializer.Deserialize>(h.GetRawText(), Constants._jsonOptions); + } + + var httpMethod = new HttpMethod(httpMethodStr.ToUpper()); + var response = await HttpHelper.FetchAsync(url, httpMethod, body, headers); + + string content = await response.Content.ReadAsStringAsync(); + + return new { + status = (int)response.StatusCode, + success = response.IsSuccessStatusCode, + data = content + }; + } catch (Exception ex) { + return new { success = false, error = ex.Message }; + } + } +} \ No newline at end of file diff --git a/src/main/services/Requirements.cs b/src/main/services/Requirements.cs new file mode 100644 index 0000000..62642e5 --- /dev/null +++ b/src/main/services/Requirements.cs @@ -0,0 +1,96 @@ +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.RegularExpressions; +using BrikInstaller.Schemas; +using BrikInstaller.Utils; +using Photino.NET; + +namespace BrikInstaller.Services; + +public static class RequirementsService { + public static async Task CheckRequirementsAsync(PhotinoWindow window, string softName) { + try { + string requirementsUrl = $"https://brik.azures.fr/products/{softName}/{Constants.os}-{Constants.arch}/requirements"; + + var response = await HttpHelper.FetchAsync(requirementsUrl, HttpMethod.Get); + + if (!response.IsSuccessStatusCode) { + return new RequirementCheckingResponse { Success = false, Status = (int)response.StatusCode }; + } + + string jsonResponse = await response.Content.ReadAsStringAsync(); + var requirements = JsonSerializer.Deserialize>(jsonResponse, Constants._jsonOptions); + + if (requirements != null) { + if (requirements.Count == 0) { + return new RequirementCheckingResponse{ Success = true, hasNoDeps = true }; + } + foreach (var element in requirements) { + IPC.sendRequirementCheckConsoleLog(window, $"{element.Name}"); + int state = await ProcessRequirement(element); + switch (state) { + case 0: + IPC.sendRequirementCheckConsoleLog(window, $" {element.Name}"); + IPC.sendRequirementCheckConsoleLog(window, $"{element.Name}"); + IPC.sendRequirementCheckConsoleLog(window, $""); + return new RequirementCheckingResponse{ Success = false }; + case 1: + IPC.sendRequirementCheckConsoleLog(window, $" {element.Name}"); + IPC.sendRequirementCheckConsoleLog(window, $"{element.Name}"); + return new RequirementCheckingResponse{ Success = false }; + case 2: + IPC.sendRequirementCheckConsoleLog(window, $""); + break; + } + } + } + return new RequirementCheckingResponse{ Success = true }; + } catch (Exception ex) { + Console.WriteLine($"[RequirementsService] Error: {ex.Message}"); + return new RequirementCheckingResponse{ Success = false, Error = ex.Message }; + } + } + + public static async Task ProcessRequirement(ProductRequirement requirement) { + string checkOutput = BashUtils.GetCommandOutput(requirement.CheckCommand!); + bool isPresent = Regex.IsMatch(checkOutput, requirement.ExpectedOutputPattern!, RegexOptions.IgnoreCase); + if (!isPresent) { + bool isAutoinstalled = await RequirementInstallationService.InstallAsync(requirement); + return isAutoinstalled == true ? 1 : 0; + } else { + return 2; + } + } +} + +public static class RequirementInstallationService { + public static async Task InstallAsync(ProductRequirement req) { + if (string.IsNullOrEmpty(req.InstallationUrl)) return false; + + if (req.InstallationMode == "manual") { + BashUtils.OpenUrl(req.InstallationUrl); + return false; + } + + if (req.InstallationMode == "auto") { + try { + var response = await HttpHelper.FetchAsync(req.InstallationUrl); + if (!response.IsSuccessStatusCode) return false; + + string extension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".ps1" : ".sh"; + string tempPath = Path.Combine(Path.GetTempPath(), $"brik_install_{req.Id}{extension}"); + + await File.WriteAllBytesAsync(tempPath, await response.Content.ReadAsByteArrayAsync()); + + string cmd = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? $"powershell -ExecutionPolicy Bypass -File \"{tempPath}\"" + : $"chmod +x \"{tempPath}\" && \"{tempPath}\""; + + BashUtils.GetCommandOutput(cmd); + return true; + } catch { return false; } + } + + return false; + } +} \ No newline at end of file diff --git a/src/resources/build/icon.ico b/src/resources/build/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c09b5305d96f3c42ba75664dd14e0c845d0a11df GIT binary patch literal 110640 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%0AA`fyo$m;%Z`*vl{={NVa^E=igtn9QV?e0PcG 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 Qv#EfFik5PTqE*QM19{_QY5)KL literal 0 HcmV?d00001 diff --git a/src/resources/build/logo.png b/src/resources/build/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..71f71b7f077d560f2efa880ea20cd28b270e42ad GIT binary patch literal 1253 zcmZ`(eNa+a6n~D>_K>JoYAF@^bo|KdYapP_C?BL@N+nj(heU0rv!=FIuIOr8516gC zFH1o!V<>62M3+9W#S7Ndd5ybnJI3<}_gK=>`>m-K7eJ!Yh|AYqeSs1k1|G zvL2P({;P>frN--oJv}`g9UTUPL7`Bb`J*~2d&P=#iI(?PbF@`}r=uK9=J5eM`~R;| zCYb>M-eX)&Y|8glLvK32Gp~W-YKx?GH?!%5CtPadaFAGxzF?=W^0>Nx#`*HZ1Le}qs+?bL0;-| zv@=Bid@g6-x+6q+}CWP%}~P8s1d4x z4vPV0kYSUkv|8)uv%Ni~LS}^G@TOg9kn{eQt;{MXoz!6(U04a@c_c=t*Zz_uvRjU{ z8EBHIXGBS!GqFqkFSr?bFRrdyN0732N0mebH|a!e$CV`y-@n^V^G~ zBQ+emCVu`_$N0!Dm^~L^HxO(FcyK^Llv$rHU(-N2w8Q^nQ}syZD{;4podGiNaiU2} z;+WlK8ZZ4yflG5ROQ>2<)5|yycLgHs07-5Nc{V~ExFzIC7G#9$2lk2l7X z<=oS)<{ADohr94X0rzK$+LjeelEaii?zw`yhpY(qT4iHGvu=Xm4nID%fyI&U-(#ZR zBOvTZ_*Qwu_*v;51vj%IEqQ-zhv~_6TH}UeY5OED)8qW8U>aHprQ?%|y?_=k<>g(g zK9it#p6}y#4Qvw>gsm@jZ2o$YSf@qn@#2wuoQ@O4teA5bLbFTKoSAc@qmsC%SzN|@zE{YncYQi-_41>#CC-0YM+MB z>Cbs3GyXy=)#>nCS5EfCem#rQvVDq#Hgu&v#joF$8}*FcK%EphNtAOnf;2E2`xH^9 nx=>g_Lmi0RBai_{s($sC?A?n`se@nO{{_HZ%j0N}T}S@~xQtW< literal 0 HcmV?d00001 diff --git a/src/resources/renderer/assets/css/common.css b/src/resources/renderer/assets/css/common.css new file mode 100644 index 0000000..ceaf9ef --- /dev/null +++ b/src/resources/renderer/assets/css/common.css @@ -0,0 +1,166 @@ +@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"); + +* { + margin: 0px; + padding: 0px; + user-select: none; + box-sizing: border-box; +} + +main { + position: absolute; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; + overflow: hidden; +} + +aside { + width: 100%; + height: 100%; +} + +aside > article.frame { + width: 100%; + height: 100%; + color: #ffffff; + padding: 17px 17px 17px 17px; + font-family: "Poppins", sans-serif; + background-color: #212121; +} + +img { + pointer-events: none; +} + +button { + font-family: "Poppins", sans-serif; +} + +p { + line-height: 20px; +} + +a { + color: #dfa615; + cursor: pointer; + font-size: 12px; + transition: .3s color; +} + +a:hover { + color: #c99001; +} + +a:active { + color: #ffb700; +} + +input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + + width: 16px; + height: 16px; + background-color: #2a2a2a; + border: 1px solid #333; + border-radius: 4px; + cursor: pointer; + position: relative; +} + +input[type="checkbox"]:checked { + background-color: #ffb700; + border-color: #ffb700; +} + +input[type="checkbox"]:checked::after { + content: ""; + position: absolute; + top: 1px; + left: 4px; + width: 4px; + height: 8px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +input[type=text] { + width: 100%; + height: 24px; + border: none; + outline: none; + padding: 0px 7px; + border-radius: 7px; +} + +div.loader { + overflow: hidden; + position: absolute; + left: 0px; + right: 0px; + bottom: 0px; + width: 100%; + height: 6px; +} + +div.loader > div.full { + height: 6px; + width: 100%; +} + +div.loader > div.full > div.loading { + height: 6px; + width: 50%; + transition: 1s width; + background-color: #dfa615; +} + +div.loader > div.full > div.loading { + animation: animateLoadingEffect 0.7s linear infinite; +} + +@keyframes animateLoadingEffect { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(200%); + } +} + +.fi-es { + transform: scale(1.02); + transform-origin: center; + image-rendering: -webkit-optimize-contrast; +} + +.fi { + backface-visibility: hidden; + -webkit-font-smoothing: antialiased; +} + +[hidden] { + display: none !important; + visibility: hidden !important; +} + +::-webkit-scrollbar { + width: 5px; + height: 5px; +} + +::-webkit-scrollbar-track { + background-color: transparent; +} + +::-webkit-scrollbar-thumb { + background-color: #ffffff3d; + border-radius: 5px; +} + +::-webkit-scrollbar-thumb:hover { + background-color: #ffffff5e; +} diff --git a/src/resources/renderer/assets/css/index.css b/src/resources/renderer/assets/css/index.css index 055c3c5..8b01fe0 100644 --- a/src/resources/renderer/assets/css/index.css +++ b/src/resources/renderer/assets/css/index.css @@ -1,45 +1,151 @@ -@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"); - -* { - margin: 0px; - padding: 0px; - box-sizing: border-box; +h1 { + font-weight: 550; } -main { - display: flex; - position: absolute; - top: 0px; - left: 0px; - right: 0px; - bottom: 0px; - flex-wrap: wrap; - flex-direction: column; - gap: 10px; - color: #cecece; - background-color: #404551; +details > summary { + font-size: small; + font-weight: 500px; +} + +details { + width: 100%; +} + +details.language { + margin-top: 10px; +} + +aside > article.frame > section.author { + display: inline-flex; align-items: center; - justify-content: center; - font-family: "Poppins", sans-serif; + position: fixed; + bottom: 10px; + left: 10px; +} + +aside > article.frame > section.author > img { + width: 25px; + height: 25px; + margin-right: 5px; + border-radius: 5px; +} + +aside > article.frame > section.author > p { + font-size: 12px; +} + +aside > article.frame > section.buttons, +aside > article.frame > section.extra_buttons { + display: inline-flex; + align-items: center; + flex-direction: row-reverse; + position: fixed; + bottom: 10px; + right: 10px; } button { + height: 25px; + min-width: 50px; + padding-left: 10px; + padding-right: 10px; cursor: pointer; - padding: 7px 7px 7px 7px; - color: #cecece; - font-weight: 500; border: none; outline: none; - border-radius: 5px; - background-color: #c8850a; - transition: .3s all; - font-family: "Poppins", sans-serif; + color: #ffffff; + border-radius: 3px; + font-weight: 500; + transition: .3s background-color; + margin-left: 4px; + margin-right: 4px; + background-color: #303030; } button:hover { - filter: brightness(0.95); + background-color: #3a3a3a; } -button:active { - filter: brightness(1.10); +button.primary { + background-color: #dfa615; } + +button.primary:hover { + background-color: #c99001; +} + +div.console { + background-color: #1b1b1b; + width: 100%; + height: 120px; + font-size: small; + padding: 7px 7px 7px 7px; + margin-top: 14px; + border-radius: .3s; + border-radius: 7px; + overflow-y: auto; + border: 1px solid #333; +} + +article.frame.eula > div.console { + white-space: pre-wrap; + word-wrap: break-word; + font-family: monospace; +} + +input[type=text] { + color: #cecece; + background-color: #1b1b1b; +} + +article.frame.choosePath > div.installation_path { + display: flex; + align-items: center; + width: 100%; + height: 25px; + margin-top: 10px; + box-sizing: border-box; +} + +article.frame.choosePath > div.installation_path > input[type=text] { + height: 100%; + font-size: 12px; + border-radius: 3px; + border: 1px solid #333; + flex: 1; + margin-top: 0; + box-sizing: border-box; +} + +article.frame.choosePath > div.installation_path > button { + height: 100%; + margin-left: 5px; + margin-right: 0; + width: auto; + box-sizing: border-box; +} + +div.title { + display: inline-flex; + align-items: center; + width: 100%; + height: 32px; +} + +div.title > img { + width: 32px; + height: 32px; + margin-right: 6px; +} + +form > div { + display: inline-flex; + align-items: center; + flex-wrap: wrap; + margin-top: 10px; +} + +form > div > label { + font-size: 14px; + margin-left: 6px; + margin-top: 1px; +} \ No newline at end of file diff --git a/src/resources/renderer/assets/css/select.css b/src/resources/renderer/assets/css/select.css new file mode 100644 index 0000000..6175c4e --- /dev/null +++ b/src/resources/renderer/assets/css/select.css @@ -0,0 +1,76 @@ +::marker { + content: ""; +} + +::-webkit-details-marker { + display: none; +} + +details { + font-family: "Poppins", sans-serif; + width: 200px; + height: 30px; + cursor: pointer; + background-color: #303030; + border-radius: 5px; + outline: none; +} + +details > summary { + display: flex; + flex-wrap: wrap; + align-items: center; + height: 30px; + padding-left: 10px; + outline: none; +} + +details > section.options { + width: auto; + margin-top: 5px; + background-color: #303030; + border-radius: 5px; + overflow-y: auto; + max-height: 90px; + outline: none; +} + +details > section.options > ul > li { + display: flex; + align-items: center; + padding-left: 7px; + height: 30px; + border-radius: 5px; + transition: .3s all; +} + +details > section.options > ul > li > input[type=radio] { + visibility: hidden; + display: none; +} + +details > section.options > ul > li > label { + display: block; + width: 100%; + font-size: 14px; +} + +details > section.options > ul > li > label > img, +details > summary > img { + height: 26px; + margin-right: 7px; + margin-left: 2px; + vertical-align: middle; +} + +span.fi { + margin-right: 5px; +} + +details > section.options > ul > li:hover { + background-color: #3b3b3b8f; +} + +details > section.options > ul > li:has(input:checked) { + background-color: #3b3b3bc9; +} \ No newline at end of file diff --git a/src/resources/renderer/assets/img/logo.png b/src/resources/renderer/assets/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..71f71b7f077d560f2efa880ea20cd28b270e42ad GIT binary patch literal 1253 zcmZ`(eNa+a6n~D>_K>JoYAF@^bo|KdYapP_C?BL@N+nj(heU0rv!=FIuIOr8516gC zFH1o!V<>62M3+9W#S7Ndd5ybnJI3<}_gK=>`>m-K7eJ!Yh|AYqeSs1k1|G zvL2P({;P>frN--oJv}`g9UTUPL7`Bb`J*~2d&P=#iI(?PbF@`}r=uK9=J5eM`~R;| zCYb>M-eX)&Y|8glLvK32Gp~W-YKx?GH?!%5CtPadaFAGxzF?=W^0>Nx#`*HZ1Le}qs+?bL0;-| zv@=Bid@g6-x+6q+}CWP%}~P8s1d4x z4vPV0kYSUkv|8)uv%Ni~LS}^G@TOg9kn{eQt;{MXoz!6(U04a@c_c=t*Zz_uvRjU{ z8EBHIXGBS!GqFqkFSr?bFRrdyN0732N0mebH|a!e$CV`y-@n^V^G~ zBQ+emCVu`_$N0!Dm^~L^HxO(FcyK^Llv$rHU(-N2w8Q^nQ}syZD{;4podGiNaiU2} z;+WlK8ZZ4yflG5ROQ>2<)5|yycLgHs07-5Nc{V~ExFzIC7G#9$2lk2l7X z<=oS)<{ADohr94X0rzK$+LjeelEaii?zw`yhpY(qT4iHGvu=Xm4nID%fyI&U-(#ZR zBOvTZ_*Qwu_*v;51vj%IEqQ-zhv~_6TH}UeY5OED)8qW8U>aHprQ?%|y?_=k<>g(g zK9it#p6}y#4Qvw>gsm@jZ2o$YSf@qn@#2wuoQ@O4teA5bLbFTKoSAc@qmsC%SzN|@zE{YncYQi-_41>#CC-0YM+MB z>Cbs3GyXy=)#>nCS5EfCem#rQvVDq#Hgu&v#joF$8}*FcK%EphNtAOnf;2E2`xH^9 nx=>g_Lmi0RBai_{s($sC?A?n`se@nO{{_HZ%j0N}T}S@~xQtW< literal 0 HcmV?d00001 diff --git a/src/resources/renderer/assets/js/common.js b/src/resources/renderer/assets/js/common.js new file mode 100644 index 0000000..65beb38 --- /dev/null +++ b/src/resources/renderer/assets/js/common.js @@ -0,0 +1,43 @@ +function getPathSeparator() { + const isWindows = navigator.platform.toLowerCase() == "win32" + return isWindows ? "\\" : "/" +} + +const separator = getPathSeparator() + +function toggleLoadingBar() { + const loader = document.querySelector("article.frame:not([hidden]) > div.loader") + if (loader.hasAttribute("hidden")) { + loader.removeAttribute("hidden") + } else { + loader.setAttribute("hidden", "") + } +} + +function showLoadingBar() { + const loader = document.querySelector("article.frame:not([hidden]) > div.loader") + if (loader.hasAttribute("hidden")) { + loader.removeAttribute("hidden") + } +} + +function hideLoadingBar() { + const loader = document.querySelector("article.frame:not([hidden]) > div.loader") + if (!loader.hasAttribute("hidden")) { + loader.setAttribute("hidden", "") + } +} + +window.addEventListener("wheel", (e) => { + if (e.ctrlKey) { + e.preventDefault() + } +}, { passive: false }) + +window.onpopstate = () => { + window.history.pushState(null, document.title, window.location.href) +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} \ No newline at end of file diff --git a/src/resources/renderer/assets/js/frames.js b/src/resources/renderer/assets/js/frames.js new file mode 100644 index 0000000..ab228c9 --- /dev/null +++ b/src/resources/renderer/assets/js/frames.js @@ -0,0 +1,32 @@ +const frames = document.querySelectorAll("article.frame") +const frameCallbacks = {} + +function showFrame(frameIdentifier) { + for (const frame of frames) { + frame.setAttribute("hidden", "") + if (frame.classList.contains(frameIdentifier)) { + frame.removeAttribute("hidden") + if (Array.isArray(frameCallbacks[frameIdentifier])) { + for (const callback of frameCallbacks[frameIdentifier]) { + callback() + } + } + } + } +} + +function showFrameNavigationButtons() { + document.querySelector("article.frame:not([hidden]) > section.buttons").removeAttribute("hidden") +} + +function hideFrameNavigationButtons() { + document.querySelector("article.frame:not([hidden]) > section.buttons").setAttribute("hidden", "") +} + +function onFrameShowed(frameIdentifier, cb) { + if (!frameCallbacks[frameIdentifier]) { + frameCallbacks[frameIdentifier] = [] + } + + frameCallbacks[frameIdentifier].push(cb) +} \ No newline at end of file diff --git a/src/resources/renderer/assets/js/index.js b/src/resources/renderer/assets/js/index.js index b6e9954..22aecb8 100644 --- a/src/resources/renderer/assets/js/index.js +++ b/src/resources/renderer/assets/js/index.js @@ -1,4 +1,274 @@ -async function callCSharp() { - const result = await system.call("hello::world") - alert(result) -} \ No newline at end of file +let products = [] + +i18next + .use(i18nextHttpBackend) + .init({ + lng: "fr", + fallbackLng: "fr", + backend: { + loadPath: "./assets/locales/{{lng}}.json", + } + }, (err, t) => { + updatePageContent() + }) + +function updatePageContent() { + document.querySelectorAll("[data-i18n]").forEach(el => { + let key = el.getAttribute("data-i18n") + + if (key.startsWith("[")) { + const parts = key.split("]") + const attr = parts[0].substring(1) + const actualKey = parts[1] + el.setAttribute(attr, i18next.t(actualKey)) + } else { + el.innerText = i18next.t(key) + } + }) +} + +async function chooseLanguage(dropdownSelector, option) { + const select = document.querySelector(`details.${dropdownSelector}`) + await choose(dropdownSelector, option) + i18next.changeLanguage(select.getAttribute("value"), () => { + updatePageContent() + }) +} + +async function fetchProducts() { + const result = await system.call("network::fetch", { + url: "https://brik.azures.fr/products/", + method: "GET", + headers: { + "Accept": "application/json" + } + }) + if (!result.success) { + return console.error("[N::F>(GP)] Error !") + } + + const data = JSON.parse(result.data) + + for (const productName of data) { + products.push({ + name: productName, + ...await fetchInstallerResourcesFor(productName) + }) + } + + await makeProductList() +} + +async function fetchInstallerResourcesFor(productName) { + const result = await system.call("network::fetch", { + url: `https://brik.azures.fr/products/${productName}/installerResource`, + method: "GET", + headers: { + "Accept": "application/json" + } + }) + if (!result.success) { + return console.error("[N::F>(GIR)] Error !") + } + + return JSON.parse(result.data) +} + +async function makeProductList() { + const productDropdown = document.querySelector("details.product") + const ul = document.createElement("ul") + for (const product of products) { + const index = products.indexOf(product) + const li = document.createElement("li") + const radio = document.createElement("input") + const label = document.createElement("label") + const logo = document.createElement("img") + const title = document.createElement("span") + + radio.setAttribute("type", "radio") + radio.setAttribute("name", "radio") + radio.setAttribute("value", index) + radio.setAttribute("id", `box_soft_${index}`) + radio.setAttribute("onclick", "choose('product', this)") + + logo.setAttribute("src", product.logo) + + title.innerText = product.name + + label.setAttribute("for", `box_soft_${index}`) + label.appendChild(logo) + label.appendChild(title) + + li.appendChild(radio) + li.appendChild(label) + + ul.appendChild(li) + } + const options = productDropdown.querySelector("section.options > ul") + options.innerHTML = ul.innerHTML +} + +function clearLogIntoRequirementConsole() { + const logsContainer = document.querySelector("article.frame.requirements > div.console") + logsContainer.innerHTML = "" +} + +function putLogIntoRequirementConsole(log) { + const logsContainer = document.querySelector("article.frame.requirements > div.console") + const code = document.createElement("code") + code.innerHTML = log + logsContainer.appendChild(code) + logsContainer.innerHTML += "
" +} + +function showFrameExtraButtons() { + document.querySelector("article.frame:not([hidden]) > section.extra_buttons").removeAttribute("hidden") +} + +function hideFrameExtraButtons() { + document.querySelector("article.frame:not([hidden]) > section.extra_buttons").setAttribute("hidden", "") +} + +function getSelectedSoftware() { + const productDropdown = document.querySelector("details.product") + const productIndex = productDropdown.getAttribute("value") + 1 + const productName = productDropdown.querySelector(`li:nth-of-type(${productIndex})`).children[1].innerText + return productName +} + +function filterProductArray(productName) { + products = products.filter(product => product.name == productName) +} + +function writeEula(eula) { + const eulaContainer = document.querySelector("article.frame.eula > div.console") + const pre = document.createElement("pre") + pre.innerText = eula + eulaContainer.appendChild(pre) +} + +function clearEula() { + const eulaContainer = document.querySelector("article.frame.eula > div.console") + eulaContainer.innerHTML = "" +} + +async function fetchEula() { + const eulaUrl = products[0].eula + const result = await system.call("network::fetch", { + url: eulaUrl, + method: "GET", + headers: { + "Accept": "application/json" + } + }) + if (!result.success) { + return console.error("[N::F>(GE)] Error !") + } + return result +} + +async function openEULA() { + await system.call("browser::open", { url: products[0].eula }) +} + +async function selectDirectory() { + const productName = getSelectedSoftware() + const result = await system.call("dialog::selectDirectory") + if (result.path != null) { + if (!result.path.endsWith(productName)) { + result.path = result.path + separator + productName + } + installationPath.setAttribute("value", result.path) + } +} + +async function checkInstallationPath(path) { + const result = await system.call("installer::testInstallationPath", { path }) + if (result.success) { + showFrameNavigationButtons() + } +} + +onFrameShowed("onboarding", () => { + const languageDropdown = document.querySelector("details.language") + if (languageDropdown.getAttribute("value") == null) { + hideFrameNavigationButtons() + } +}) + +onFrameShowed("chooseProduct", () => { + const productsDropdown = document.querySelector("details.product") + const productIndex = productsDropdown.getAttribute("value") + if (productIndex != null) { + const product = productsDropdown.querySelector(`li:nth-of-type(${productIndex + 1})`).children[0] + choose("product", product) + } else { + hideFrameNavigationButtons() + showFrameExtraButtons() + } +}) + +onFrameShowed("requirements", async () => { + const productName = getSelectedSoftware() + clearLogIntoRequirementConsole() + hideFrameNavigationButtons() + hideFrameExtraButtons() + showLoadingBar() + filterProductArray(productName) + await system.call("check::requirements", { soft: productName }) +}) + +onFrameShowed("eula", async () => { + clearEula() + const eula = await fetchEula() + writeEula(eula.data) +}) + +onFrameShowed("choosePath", async () => { + hideFrameNavigationButtons() + const result = await system.call("installer::defaultInstallPath", { soft: getSelectedSoftware() }) + if (result.success) { + installationPath.setAttribute("value", result.path) + installationPath.onchange() + } +}) + +onFrameShowed("download", async () => { + hideFrameExtraButtons() + hideFrameNavigationButtons() + showLoadingBar() + const result = await system.call("installer::install", { soft: getSelectedSoftware(), path: installationPath.getAttribute("value") }) + hideLoadingBar() + if (result.success) { + showFrame("end") + } else { + showFrameExtraButtons() + } +}) + +onSelected("language", () => { + showFrameNavigationButtons() +}) + +onSelected("product", () => { + hideFrameExtraButtons() + showFrameNavigationButtons() +}) + +system.result("requirement::log", (data) => { + putLogIntoRequirementConsole(data.message) + updatePageContent() +}) + +system.result("requirement::failed", () => { + hideLoadingBar() + showFrameExtraButtons() +}) + +system.result("requirement::success", () => { + hideLoadingBar() + showFrameNavigationButtons() +}) + +showFrame("onboarding") +fetchProducts() \ No newline at end of file diff --git a/src/resources/renderer/assets/js/ipc.js b/src/resources/renderer/assets/js/ipc.js index 6f54d74..01305d0 100644 --- a/src/resources/renderer/assets/js/ipc.js +++ b/src/resources/renderer/assets/js/ipc.js @@ -1,14 +1,19 @@ const system = { _pendingRequests: new Map(), + _listeners: new Map(), init: function() { window.external.receiveMessage(response => { try { const res = JSON.parse(response) if (res.requestId && this._pendingRequests.has(res.requestId)) { - const { resolve } = this._pendingRequests.get(res.requestId) - this._pendingRequests.delete(res.requestId) - resolve(res.payload) + const { resolve } = this._pendingRequests.get(res.requestId); + this._pendingRequests.delete(res.requestId); + resolve(res.payload); + } + else if (res.requestId && this._listeners.has(res.requestId)) { + const callback = this._listeners.get(res.requestId); + callback(res.payload); } } catch (e) { console.error("Erreur format message :", e) @@ -28,7 +33,35 @@ const system = { payload: data })) }) + }, + + result: function(eventName, callback) { + this._listeners.set(eventName, callback); } } +window.external.receiveMessage(message => { + try { + const data = JSON.parse(message) + + switch (data.requestId) { + case "game::log": + const logMessage = data.payload.message + if (logMessage != "GAME_CLOSED") { + console.log("MC:", logMessage) + gamelog.put(logMessage) + logsContainer.scrollTop = logsContainer.scrollHeight + } else { + playButton.removeAttribute("disabled") + } + break; + + default: + break; + } + } catch (e) { + console.error("Erreur réception message Photino:", e) + } +}) + system.init() \ No newline at end of file diff --git a/src/resources/renderer/assets/js/select.js b/src/resources/renderer/assets/js/select.js new file mode 100644 index 0000000..2890703 --- /dev/null +++ b/src/resources/renderer/assets/js/select.js @@ -0,0 +1,26 @@ +const selectCallbacks = {} + +function onSelected(dropdownSelector, cb) { + if (!selectCallbacks[dropdownSelector]) { + selectCallbacks[dropdownSelector] = [] + } + + selectCallbacks[dropdownSelector].push(cb) +} + +async function choose(dropdownSelector, option) { + const select = document.querySelector(`details.${dropdownSelector}`) + const title = document.querySelector(`details.${dropdownSelector}>summary`) + const value = document.querySelector(`label[for=${option.id}]`) + title.innerHTML = value.innerHTML + if (select.getAttribute("value") != option.getAttribute("value")) { + await sleep(115) + } + select.setAttribute("value", option.getAttribute("value")) + select.removeAttribute("open") + if (Array.isArray(selectCallbacks[dropdownSelector])) { + for (const callback of selectCallbacks[dropdownSelector]) { + callback(option.getAttribute("value")) + } + } +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/arab.json b/src/resources/renderer/assets/locales/arab.json new file mode 100644 index 0000000..ea4bc5f --- /dev/null +++ b/src/resources/renderer/assets/locales/arab.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "rtl", + "description": "مرحباً بك في مساعد تثبيت Brik", + "next": "التالي", + "eula": "يرجى قراءة والموافقة على شروط الاستخدام", + "exit": "خروج / إنهاء", + "retry": "إعادة المحاولة", + "accept": "أوافق", + "browse": "تصفح", + "install": "تثبيت", + "previous": "السابق", + "downloading": "جاري تحميل ملفات البرنامج", + "choose_path": "اختر موقع التثبيت", + "create_shortcut": "إنشاء اختصار على سطح المكتب", + "launch_after_exit": "تشغيل البرنامج بعد إغلاق المثبت", + "installation_path": "اختر مجلد التثبيت", + "check_eula_online": "عرض الترخيص في المتصفح", + "select_soft_to_install": "اختر البرنامج الذي تود تثبيته", + "requirements": "التحقق من المكونات المطلوبة", + "log_checking_requirement": "جاري فحص ", + "log_checking_requirement_success": "✅ المكون موجود", + "log_checking_requirement_error": "❌ مكون ناقص ", + "log_checking_requirement_to_manual_install": "يرجى تثبيت المكون الناقص ", + "log_checking_requirement_to_manual_install_1": "ستفتح صفحة التحميل الآن", + "log_checking_requirement_attempt_to_auto_install": "محاولة التثبيت التلقائي: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/dz.json b/src/resources/renderer/assets/locales/dz.json new file mode 100644 index 0000000..a436567 --- /dev/null +++ b/src/resources/renderer/assets/locales/dz.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Merhba bik fi moussa3id installation Brik", + "next": "Suivant", + "eula": "Y3aychek iqra w wafeq 3la chorout el-isti3mal", + "exit": "Khroj / Kmel", + "retry": "3awed jareb", + "accept": "Nwafeq", + "browse": "Parcourir", + "install": "Installi", + "previous": "Lback / Précédent", + "downloading": "Rani n-téléchargi fel-programme", + "choose_path": "Khayer win t-installi el-programme", + "create_shortcut": "Dir raccourci f-el-bureau", + "launch_after_exit": "7el el-programme ki t-oghlaq l-installateur", + "installation_path": "Khayer el-dossier ta3 l-installation", + "check_eula_online": "Chouf el-contrat f-el-navigateur", + "select_soft_to_install": "Khayer el-logiciel li rak hab t-installih", + "requirements": "N-verifyiw f-el-7ajet li lazem bech yekhdem", + "log_checking_requirement": "N-verifyiw fi ", + "log_checking_requirement_success": "✅ Kolchi mriguel", + "log_checking_requirement_error": "❌ Kayna 7aja naqsa ", + "log_checking_requirement_to_manual_install": "Y3aychek installi el-7aja li naqsa ", + "log_checking_requirement_to_manual_install_1": "Taw tet7al la page ta3 el-téléchargement", + "log_checking_requirement_attempt_to_auto_install": "N-sayiw n-installouha wa7edna: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/en.json b/src/resources/renderer/assets/locales/en.json new file mode 100644 index 0000000..b6da80b --- /dev/null +++ b/src/resources/renderer/assets/locales/en.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Welcome to the Brik installation wizard", + "next": "Next", + "eula": "Please read and accept the terms of use.", + "exit": "Quit", + "retry": "Retry", + "accept": "I accept", + "browse": "Browse", + "install": "Install", + "previous": "Previous", + "downloading": "Program files download in progress", + "choose_path": "Select the installation location", + "create_shortcut": "Create a shortcut on the desktop", + "launch_after_exit": "Launch program after closing the installer", + "installation_path": "Select the installation location", + "check_eula_online": "View the licence in the browser", + "select_soft_to_install": "Select the software you wish to install?", + "requirements": "Verification of required components", + "log_checking_requirement": "Checking ", + "log_checking_requirement_success": "✅ Component present ", + "log_checking_requirement_error": "❌ Missing component ", + "log_checking_requirement_to_manual_install": "Please install the missing component ", + "log_checking_requirement_to_manual_install_1": "The download page will open", + "log_checking_requirement_attempt_to_auto_install": "Attempted automatic installation: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/es.json b/src/resources/renderer/assets/locales/es.json new file mode 100644 index 0000000..56b55af --- /dev/null +++ b/src/resources/renderer/assets/locales/es.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Bienvenido al asistente de instalación de Brik", + "next": "Siguiente", + "eula": "Por favor, lea y acepte las condiciones de uso.", + "exit": "Salir", + "retry": "Reintentar", + "accept": "Acepto", + "browse": "Examinar", + "install": "Instalar", + "previous": "Anterior", + "downloading": "Descarga de archivos del programa en curso", + "choose_path": "Seleccione la ubicación de instalación", + "create_shortcut": "Crear un acceso directo en el escritorio", + "launch_after_exit": "Ejecutar al finalizar", + "installation_path": "Seleccione la ruta de instalación", + "check_eula_online": "Ver la licencia en el navegador", + "select_soft_to_install": "¿Seleccione el software que desea instalar?", + "requirements": "Verificación de los componentes requeridos", + "log_checking_requirement": "Comprobando ", + "log_checking_requirement_success": "✅ Componente presente ", + "log_checking_requirement_error": "❌ Componente faltante ", + "log_checking_requirement_to_manual_install": "Por favor, instale el componente faltante ", + "log_checking_requirement_to_manual_install_1": "Se abrirá la página de descarga", + "log_checking_requirement_attempt_to_auto_install": "Intento de instalación automática: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/et.json b/src/resources/renderer/assets/locales/et.json new file mode 100644 index 0000000..ce115d1 --- /dev/null +++ b/src/resources/renderer/assets/locales/et.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "ወደ Brik የመጫኛ ረዳት በደህና መጡ", + "next": "ቀጣይ", + "eula": "እባክዎ የአጠቃቀም ደንቦችን ያንብቡ እና ይስማሙ", + "exit": "ውጣ / ጨርስ", + "retry": "እንደገና ሞክር", + "accept": "እስማማለሁ", + "browse": "ፈልግ", + "install": "ጫን", + "previous": "ተመለስ", + "downloading": "የፕሮግራም ፋይሎች በመውረድ ላይ ናቸው", + "choose_path": "የመጫኛ ቦታውን ይምረጡ", + "create_shortcut": "በዴስክቶፕ ላይ አቋራጭ ፍጠር", + "launch_after_exit": "መጫኛው ሲዘጋ ፕሮግራሙን ክፈት", + "installation_path": "የመጫኛ ማህደሩን ይምረጡ", + "check_eula_online": "ፈቃዱን በአሳሽ ላይ ይመልከቱ", + "select_soft_to_install": "ሊጭኑት የሚፈልጉትን ሶፍትዌር ይምረጡ", + "requirements": "አስፈላጊ አካላትን በማረጋገጥ ላይ", + "log_checking_requirement": "በመፈተሽ ላይ: ", + "log_checking_requirement_success": "✅ አካሉ ተገኝቷል", + "log_checking_requirement_error": "❌ የጎደለ አካል አለ", + "log_checking_requirement_to_manual_install": "እባክዎ የጎደለውን አካል ይጫኑ: ", + "log_checking_requirement_to_manual_install_1": "የማውረጃው ገጽ አሁን ይከፈታል", + "log_checking_requirement_attempt_to_auto_install": "ራሱን በራሱ ለመጫን እየሞከረ ነው: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/fr.json b/src/resources/renderer/assets/locales/fr.json new file mode 100644 index 0000000..f266b82 --- /dev/null +++ b/src/resources/renderer/assets/locales/fr.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Bienvenue dans l'assistant d'installation Brik", + "next": "Suivant", + "eula": "Veuillez lire et accepter les conditions d'utilisation", + "exit": "Quitter / Terminer", + "retry": "Réessayer", + "accept": "J'accepte", + "install": "Installer", + "browse": "Parcourir", + "previous": "Précédent", + "downloading": "Téléchargement du programme en cours", + "choose_path": "Choisissez l'emplacement d'installation", + "create_shortcut": "Créer un raccourci sur le bureau", + "launch_after_exit": "Lancer le programme après la fermeture de l'installateur", + "installation_path": "Sélectionnez le dossier d'installation", + "check_eula_online": "Consulter la licence dans le navigateur", + "select_soft_to_install": "Choisissez le logiciel que vous souhaitez installer", + "requirements": "Vérification des composants requis", + "log_checking_requirement": "Vérification de ", + "log_checking_requirement_success": "✅ Composant présent", + "log_checking_requirement_error": "❌ Composant manquant ", + "log_checking_requirement_to_manual_install": "Veuillez installer le composant manquant ", + "log_checking_requirement_to_manual_install_1": "La page de téléchargement va s'ouvrir", + "log_checking_requirement_attempt_to_auto_install": "Tentative d'installation automatique : " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/it.json b/src/resources/renderer/assets/locales/it.json new file mode 100644 index 0000000..6a90b25 --- /dev/null +++ b/src/resources/renderer/assets/locales/it.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Benvenuto nella procedura guidata di installazione di Brik", + "next": "Successivo", + "eula": "Leggi e accetta i termini di utilizzo.", + "exit": "Esci", + "retry": "Retry", + "accept": "Riprova", + "browse": "Sfoglia", + "install": "Installare", + "previous": "Precedente", + "downloading": "Download dei file di programma in corso", + "choose_path": "Selezionare la posizione di installazione", + "create_shortcut": "Creare un collegamento sul desktop", + "launch_after_exit": "Apri dopo l'installazione", + "installation_path": "Selezionare la posizione di installazione", + "check_eula_online": "Visualizza la licenza nel browser", + "select_soft_to_install": "Selezionare il software che si desidera installare?", + "requirements": "Verifica dei componenti richiesti", + "log_checking_requirement": "Controllo ", + "log_checking_requirement_success": "✅ Componente presente ", + "log_checking_requirement_error": "❌ Componente mancante ", + "log_checking_requirement_to_manual_install": "Si prega di installare il componente mancante ", + "log_checking_requirement_to_manual_install_1": "Si aprirà la pagina di download", + "log_checking_requirement_attempt_to_auto_install": "Tentativo di installazione automatica: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/jp.json b/src/resources/renderer/assets/locales/jp.json new file mode 100644 index 0000000..3144c63 --- /dev/null +++ b/src/resources/renderer/assets/locales/jp.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Brik インストールウィザードへようこそ", + "next": "次へ", + "eula": "利用規約を読み、同意してください。", + "exit": "終了", + "retry": "再試行", + "accept": "同意する", + "browse": "参照", + "install": "インストール", + "previous": "戻る", + "downloading": "プログラムファイルをダウンロード中...", + "choose_path": "インストール先を選択してください", + "create_shortcut": "デスクトップにショートカットを作成する", + "launch_after_exit": "終了後にプログラムを起動する", + "installation_path": "インストール先のフォルダを選択", + "check_eula_online": "ブラウザでライセンスを確認する", + "select_soft_to_install": "インストールするソフトウェアを選択してください", + "requirements": "必須コンポーネントの確認中", + "log_checking_requirement": "確認中: ", + "log_checking_requirement_success": "✅ コンポーネントが見つかりました", + "log_checking_requirement_error": "❌ コンポーネントが不足しています", + "log_checking_requirement_to_manual_install": "不足しているコンポーネントをインストールしてください: ", + "log_checking_requirement_to_manual_install_1": "ダウンロードページを開きます", + "log_checking_requirement_attempt_to_auto_install": "自動インストールを試行中: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/ma.json b/src/resources/renderer/assets/locales/ma.json new file mode 100644 index 0000000..6d86a1b --- /dev/null +++ b/src/resources/renderer/assets/locales/ma.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Merhba bik f mosa3id tatbit Brik", + "next": "Tali", + "eula": "3afak iqra w wafeq 3la chourout l-istis3mal", + "exit": "Khrouj / Salina", + "retry": "3awed jereb", + "accept": "Kanwafeq", + "browse": "Parcourir", + "install": "Installi", + "previous": "L-pari li fat", + "downloading": "Jarri l-tahmil dyal l-programme", + "choose_path": "Khtar fin t-installi l-programme", + "create_shortcut": "Dir raccourci f l-bureau", + "launch_after_exit": "Khdem l-programme melli tsed l-installateur", + "installation_path": "Khtar l-dossier dyal l-installation", + "check_eula_online": "Chouf l-license f l-navigateur", + "select_soft_to_install": "Khtar l-logiciel li bghiti t-installi", + "requirements": "Kan-verifiw l-7ajat li daroria bach ykhdem", + "log_checking_requirement": "Verificassion dyal ", + "log_checking_requirement_success": "✅ Kolchi houwa hadak", + "log_checking_requirement_error": "❌ Kayna 7aja naqsa ", + "log_checking_requirement_to_manual_install": "3afak installi had l-7aja li naqsa ", + "log_checking_requirement_to_manual_install_1": "Ghat-t-fte7 l-pari dyal l-tahmil", + "log_checking_requirement_attempt_to_auto_install": "Ghan-jerrbo n-installiwha rousna: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/ng.json b/src/resources/renderer/assets/locales/ng.json new file mode 100644 index 0000000..7968e18 --- /dev/null +++ b/src/resources/renderer/assets/locales/ng.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Ẹ kú àbọ̀ sí abẹ́yọrí ìfilọ́lẹ̀ Brik", + "next": "Tẹ̀síwájú", + "eula": "Jọ̀wọ́ ka kí o sì gbà fún àwọn òfin ì lò.", + "exit": "Jáde / Parí", + "retry": "Ìdánwò lẹ́ẹ̀kan sí i", + "accept": "Mo gbà", + "browse": "Wá kiri", + "install": "Fi bẹ̀rẹ̀", + "previous": "Padà sẹ́yìn", + "downloading": "Ìgbàsílẹ̀ àwọn fáìlì ètò ń lọ lọ́wọ́...", + "choose_path": "Yan ibi tí o fẹ́ fi ètò sí", + "create_shortcut": "Ṣẹ̀dá ọ̀nà ìkékúrú sórí ojú iṣẹ́ (desktop)", + "launch_after_exit": "Ṣí ètò lẹ́yìn tí o bá ti ti olùfilọ́lẹ̀", + "installation_path": "Yan ibi tí ètò yóò wà", + "check_eula_online": "Wo ìwé àṣẹ nínú ẹ̀rọ ìṣíwò (browser)", + "select_soft_to_install": "Yan ètò (software) tí o fẹ́ fi bẹ̀rẹ̀", + "requirements": "Ìmúdájú àwọn nǹkan tí ó ṣe dandan", + "log_checking_requirement": "Ìyẹ̀wò lọ́wọ́: ", + "log_checking_requirement_success": "✅ Ohun èlò ti wà", + "log_checking_requirement_error": "❌ Ohun èlò kan dín bù ", + "log_checking_requirement_to_manual_install": "Jọ̀wọ́ fi ohun èlò tí ó dín bù bẹ̀rẹ̀ ", + "log_checking_requirement_to_manual_install_1": "Ojú-ewé ìgbàsílẹ̀ yóò ṣí nísìnyí", + "log_checking_requirement_attempt_to_auto_install": "Ìgbìnyànjú láti fi bẹ̀rẹ̀ fúnrawá: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/ru.json b/src/resources/renderer/assets/locales/ru.json new file mode 100644 index 0000000..476ae70 --- /dev/null +++ b/src/resources/renderer/assets/locales/ru.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Добро пожаловать в мастер установки Brik", + "next": "Далее", + "eula": "Пожалуйста, прочтите и примите условия использования", + "exit": "Выход", + "retry": "Повторить", + "accept": "Я принимаю", + "browse": "Обзор", + "install": "Установить", + "previous": "Назад", + "downloading": "Загрузка файлов программы...", + "choose_path": "Выберите путь установки", + "create_shortcut": "Создать ярлык на рабочем столе", + "launch_after_exit": "Запустить программу после завершения", + "installation_path": "Выберите папку для установки", + "check_eula_online": "Посмотреть лицензию в браузере", + "select_soft_to_install": "Выберите компоненты для установки", + "requirements": "Проверка системных требований", + "log_checking_requirement": "Проверка: ", + "log_checking_requirement_success": "✅ Компонент найден", + "log_checking_requirement_error": "❌ Компонент отсутствует", + "log_checking_requirement_to_manual_install": "Пожалуйста, установите недостающий компонент: ", + "log_checking_requirement_to_manual_install_1": "Сейчас откроется страница загрузки", + "log_checking_requirement_attempt_to_auto_install": "Попытка автоматической установки: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/sn.json b/src/resources/renderer/assets/locales/sn.json new file mode 100644 index 0000000..e983c1d --- /dev/null +++ b/src/resources/renderer/assets/locales/sn.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Dalal jàmm ci sarsaralub instalasiyoŋu Brik", + "next": "Kanami", + "eula": "Nanga gëstu te nangu sàrti jëfandiku gi", + "exit": "Génn / Pare", + "retry": "Baraat ko", + "accept": "Nangu naa", + "browse": "Seet", + "install": "Instale", + "previous": "Ginnaaw", + "downloading": "Yebum kàggi porogaraam bi mu ngi nekk ci yoon", + "choose_path": "Tànnal bërëb bi ñuy instale porogaraam bi", + "create_shortcut": "Sudar ab ràccu ci tablo bi", + "launch_after_exit": "Ubi porogaraam bi su instalatër bi tëjoo", + "installation_path": "Tànnal kàggub instalasiyoŋ bi", + "check_eula_online": "Gëstu sàrt yi ci internet", + "select_soft_to_install": "Tànnal porogaraam bi nga bëggë instale", + "requirements": "Xayma kàggi yi ñu laaj ngir mu liggéey", + "log_checking_requirement": "Xayma bu ", + "log_checking_requirement_success": "✅ Lépp mu ngi dox", + "log_checking_requirement_error": "❌ Am na kàggi lu manke ", + "log_checking_requirement_to_manual_install": "Nanga instale kàggi li manke ", + "log_checking_requirement_to_manual_install_1": "Xëtub yeb bi dina ubeeku léegi", + "log_checking_requirement_attempt_to_auto_install": "Njeemeb instalasiyoŋ bu boppam: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/tamazight.json b/src/resources/renderer/assets/locales/tamazight.json new file mode 100644 index 0000000..e38a2a3 --- /dev/null +++ b/src/resources/renderer/assets/locales/tamazight.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Ansuf yiswen ɣer imawan n ussebded n Brik", + "next": "Afus d-iteddun", + "eula": "Ɛafak ɣer ukan tqebleḍ timentilin n usemres", + "exit": "Ffeɣ / Kmel", + "retry": "Ɛawed jareb", + "accept": "Qebleɣ", + "browse": "Snubbjet", + "install": "Ssebded", + "previous": "Amezwaru", + "downloading": "Agmar n yifuyla n uselkim", + "choose_path": "Fren amkan n ussebded", + "create_shortcut": "Snerni rracoursi di ttabla", + "launch_after_exit": "Ssekker aselkim madi tmedleḍ amezen n ussebded", + "installation_path": "Fren akaram n ussebded", + "check_eula_online": "Ẓer tasiregt di ttanfult", + "select_soft_to_install": "Fren aselkim i tebɣid ad tsebdeded", + "requirements": "Aselken n yiferdisen i d-ittwasutren", + "log_checking_requirement": "Aselken n ", + "log_checking_requirement_success": "✅ Kulci yella", + "log_checking_requirement_error": "❌ Yella wayen i khussen ", + "log_checking_requirement_to_manual_install": "Ɛafak ssebded ayen i khussen ", + "log_checking_requirement_to_manual_install_1": "Tasaft n ugmar a d-teldi tura", + "log_checking_requirement_attempt_to_auto_install": "Armud n ussebded n yiman-is: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/tn.json b/src/resources/renderer/assets/locales/tn.json new file mode 100644 index 0000000..ce688da --- /dev/null +++ b/src/resources/renderer/assets/locales/tn.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Marhba bik fi moussa3id installation Brik", + "next": "Tali", + "eula": "Y3aychek iqra w wafeq 3ala chourout el-istis3mal", + "exit": "Okhrjou / Kmal", + "retry": "3awed jareb", + "accept": "Nwafeq", + "browse": "Lawwej", + "install": "Installi", + "previous": "Litali", + "downloading": "Qa3ed ncharji fel-programme", + "choose_path": "Ekhtar win tseb el-programme", + "create_shortcut": "A3mel raccourci 3al-bureau", + "launch_after_exit": "7el el-programme ki tsekker l-installateur", + "installation_path": "Ekter el-dossier mta3 l-installation", + "check_eula_online": "Chouf el-license fel-navigateur", + "select_soft_to_install": "Ekhtar el-logiciel elli t7eb tsebbo", + "requirements": "Nthabtou fel-7ajet elli lezmin bech yekhdem", + "log_checking_requirement": "Nthabtou fi ", + "log_checking_requirement_success": "✅ Kol chay mriguel", + "log_checking_requirement_error": "❌ Famma 7aja naqsa ", + "log_checking_requirement_to_manual_install": "Y3aychek sab el-7aja elli naqsa ", + "log_checking_requirement_to_manual_install_1": "Taw tet7al saf7et el-telechargement", + "log_checking_requirement_attempt_to_auto_install": "Taw njarbou nsebboha wa7edna: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/tz.json b/src/resources/renderer/assets/locales/tz.json new file mode 100644 index 0000000..c8d8d62 --- /dev/null +++ b/src/resources/renderer/assets/locales/tz.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Karibu kwenye programu ya usakinishaji ya Brik", + "next": "Endelea", + "eula": "Tafadhali soma na ukubali masharti ya matumizi", + "exit": "Ondoka / Maliza", + "retry": "Jaribu tena", + "accept": "Ninakubali", + "browse": "Vinjari", + "install": "Sakinisha", + "previous": "Rudi nyuma", + "downloading": "Upakuaji wa faili za programu unaendelea", + "choose_path": "Chagua mahali pa kusakinisha", + "create_shortcut": "Weka njia ya mkato kwenye desktop", + "launch_after_exit": "Fungua programu baada ya kufunga kisakinishi", + "installation_path": "Chagua folda ya usakinishaji", + "check_eula_online": "Angalia leseni kwenye kivinjari", + "select_soft_to_install": "Chagua programu unayotaka kusakinisha", + "requirements": "Uhakiki wa vipengele vinavyohitajika", + "log_checking_requirement": "Inakagua ", + "log_checking_requirement_success": "✅ Kipengele kipo", + "log_checking_requirement_error": "❌ Kipengele kinakosekana ", + "log_checking_requirement_to_manual_install": "Tafadhali sakinisha kipengele kinachokosekana ", + "log_checking_requirement_to_manual_install_1": "Ukurasa wa upakuaji utafunguka", + "log_checking_requirement_attempt_to_auto_install": "Jaribio la usakinishaji wa kiotomatiki: " +} \ No newline at end of file diff --git a/src/resources/renderer/assets/locales/za.json b/src/resources/renderer/assets/locales/za.json new file mode 100644 index 0000000..6fa3a2f --- /dev/null +++ b/src/resources/renderer/assets/locales/za.json @@ -0,0 +1,26 @@ +{ + "layout_direction": "ltr", + "description": "Siyakwamukela kusisizi sokufaka se-Brik", + "next": "Okulandelayo", + "eula": "Sicela ufunde futhi ulande imigomo yokusetshenziswa", + "exit": "Phuma / Qeda", + "retry": "Zama futhi", + "accept": "Ngiyavuma", + "browse": "Bhekisisa", + "install": "Faka", + "previous": "Okwedlule", + "downloading": "Kulandwa amafayela ohlelo...", + "choose_path": "Khetha lapho uzofaka khona uhlelo", + "create_shortcut": "Yenza isinqamuleli kudeskithophu", + "launch_after_exit": "Vula uhlelo ngemuva kokuvala isifaki", + "installation_path": "Khetha ifolda yokufaka", + "check_eula_online": "Buka ilayisensi kusiphequluli", + "select_soft_to_install": "Khetha isofthiwe ofuna ukuyifaka", + "requirements": "Ukuhlola izinto ezidingekayo", + "log_checking_requirement": "Kuhlolwa ", + "log_checking_requirement_success": "✅ Konke kulungile", + "log_checking_requirement_error": "❌ Kukhona okushortayo ", + "log_checking_requirement_to_manual_install": "Sicela ufake lokhu okushortayo ", + "log_checking_requirement_to_manual_install_1": "Ikhasi lokulanda lizovuleka manje", + "log_checking_requirement_attempt_to_auto_install": "Kuzanywa ukufaka ngokuzenzakalelayo: " +} \ No newline at end of file diff --git a/src/resources/renderer/index.html b/src/resources/renderer/index.html index 0306745..2b4e3cc 100644 --- a/src/resources/renderer/index.html +++ b/src/resources/renderer/index.html @@ -3,21 +3,292 @@ + + +
-

- Bienvenue dans Photino -

- -
+
+ + + + + + +