413 lines
26 KiB
C#
413 lines
26 KiB
C#
// Offline installer and ownership-scoped uninstaller. .NET Framework is part of Windows.
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Web.Script.Serialization;
|
|
using System.Windows.Forms;
|
|
using Microsoft.Win32;
|
|
|
|
internal static class Program
|
|
{
|
|
const string Version = "@BUNDLE_VERSION@";
|
|
const string BundleHash = "@BUNDLE_HASH@";
|
|
const string Product = "YouTubeDL.Helper";
|
|
const string Host = "com.youtubedl.native";
|
|
const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\YouTubeDL.Helper";
|
|
static readonly string[] Browsers = { @"Google\Chrome", @"Microsoft\Edge", "Chromium" };
|
|
static readonly JavaScriptSerializer Json = new JavaScriptSerializer { MaxJsonLength = 16 * 1024 * 1024 };
|
|
|
|
[STAThread]
|
|
public static int Main(string[] args)
|
|
{
|
|
Console.SetOut(new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false)) { AutoFlush = true });
|
|
Console.SetError(new StreamWriter(Console.OpenStandardError(), new UTF8Encoding(false)) { AutoFlush = true });
|
|
AppContext.SetSwitch("Switch.System.IO.UseLegacyPathHandling", false);
|
|
AppContext.SetSwitch("Switch.System.IO.BlockLongPaths", false);
|
|
if (args.Contains("--pick-folder"))
|
|
{
|
|
Application.EnableVisualStyles();
|
|
using (var dialog = new FolderBrowserDialog())
|
|
using (var owner = new Form { TopMost = true, ShowInTaskbar = false, Opacity = 0 })
|
|
{
|
|
string initial = Value(args, "--initial-folder");
|
|
if (initial != null) dialog.SelectedPath = Json.Deserialize<string>(Encoding.UTF8.GetString(Convert.FromBase64String(initial)));
|
|
dialog.Description = "YouTubeDL — папка сохранения"; dialog.ShowNewFolderButton = true;
|
|
owner.Show();
|
|
Console.WriteLine(Json.Serialize(dialog.ShowDialog(owner) == DialogResult.OK ? dialog.SelectedPath : null));
|
|
return 0;
|
|
}
|
|
}
|
|
bool silent = args.Contains("--silent");
|
|
bool uninstall = args.Contains("--uninstall");
|
|
string explicitRoot = Value(args, "--target-root");
|
|
string appRoot = Path.GetFullPath(explicitRoot ?? (uninstall ? Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
|
|
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "YouTubeDL", "Helper"))).TrimEnd(Path.DirectorySeparatorChar);
|
|
Action<Action<int, string>> work = report => Run(appRoot, args, uninstall, report);
|
|
if (silent)
|
|
{
|
|
string previous = null;
|
|
try { work((percent, message) => { if (message != previous) Console.WriteLine(message); previous = message; }); return 0; }
|
|
catch (Exception error) { Console.Error.WriteLine(error.ToString()); return 1; }
|
|
}
|
|
Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false);
|
|
var window = new SetupWindow(work, uninstall);
|
|
Application.Run(window);
|
|
return window.ExitCode;
|
|
}
|
|
|
|
static string Value(string[] args, string key)
|
|
{
|
|
int index = Array.IndexOf(args, key);
|
|
return index >= 0 && index + 1 < args.Length ? args[index + 1] : null;
|
|
}
|
|
|
|
static void Run(string root, string[] args, bool uninstall, Action<int, string> report)
|
|
{
|
|
if (Path.GetPathRoot(root).TrimEnd('\\') == root.TrimEnd('\\') || root == Environment.GetFolderPath(Environment.SpecialFolder.UserProfile))
|
|
throw new Exception("Некорректная папка установки.");
|
|
if (Directory.Exists(root) && (File.GetAttributes(root) & FileAttributes.ReparsePoint) != 0)
|
|
throw new Exception("Папка установки не должна быть символической ссылкой.");
|
|
string waiting = Value(args, "--wait-pid");
|
|
if (waiting != null)
|
|
{
|
|
try { using (var parent = Process.GetProcessById(Int32.Parse(waiting))) { if (!parent.WaitForExit(30000)) throw new Exception("Помощник ещё работает. Закройте меню и повторите удаление."); } }
|
|
catch (ArgumentException) { }
|
|
}
|
|
if (!uninstall && !args.Contains("--finish-uninstall"))
|
|
{
|
|
Directory.CreateDirectory(root);
|
|
if (!args.Contains("--activate-release"))
|
|
{
|
|
// AppData virtualization can merge an existing real directory
|
|
// with newly created children elsewhere. Resolve a fresh child
|
|
// to find the directory where this install will actually write.
|
|
string probeName = ".ytdl-location-" + Guid.NewGuid().ToString("N");
|
|
string probe = Inside(root, probeName);
|
|
Directory.CreateDirectory(probe);
|
|
string physicalProbe = PhysicalDirectory(probe);
|
|
if (!String.Equals(Path.GetFileName(physicalProbe), probeName, StringComparison.OrdinalIgnoreCase))
|
|
throw new Exception("Не удалось определить папку установки.");
|
|
root = Path.GetDirectoryName(physicalProbe);
|
|
Directory.Delete(physicalProbe, false);
|
|
}
|
|
}
|
|
// Keep the manager, marker and runtime in the same physical directory.
|
|
root = PhysicalDirectory(root).TrimEnd(Path.DirectorySeparatorChar);
|
|
string key;
|
|
using (var sha = SHA256.Create()) key = BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(PhysicalDirectory(root).ToLowerInvariant()))).Replace("-", "").Substring(0, 16);
|
|
using (var mutex = new Mutex(false, "Local\\YouTubeDL.Setup." + key))
|
|
{
|
|
if (!mutex.WaitOne(0)) throw new Exception("Другая установка или удаление уже выполняется.");
|
|
try
|
|
{
|
|
if (args.Contains("--activate-release")) Activate(root, args, report);
|
|
else if (args.Contains("--finish-uninstall")) FinishRemoval(root, report);
|
|
else if (uninstall) Uninstall(root, report);
|
|
else
|
|
{
|
|
#if SETUP
|
|
Install(root, !args.Contains("--no-register"), report);
|
|
#else
|
|
throw new Exception("Для установки запустите YouTubeDL-Helper-Setup.exe.");
|
|
#endif
|
|
}
|
|
}
|
|
finally { mutex.ReleaseMutex(); }
|
|
}
|
|
}
|
|
|
|
static string Inside(string root, string relative)
|
|
{
|
|
if (String.IsNullOrWhiteSpace(relative) || Path.IsPathRooted(relative)) throw new Exception("Некорректный путь в комплекте.");
|
|
string full = Path.GetFullPath(Path.Combine(root, relative));
|
|
if (!full.StartsWith(root.TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase)) throw new Exception("Путь выходит за папку комплекта.");
|
|
for (var parent = new DirectoryInfo(Path.GetDirectoryName(full)); parent != null && parent.FullName.Length >= root.Length; parent = parent.Parent)
|
|
if (parent.Exists && (parent.Attributes & FileAttributes.ReparsePoint) != 0) throw new Exception("Символические ссылки в папке комплекта не поддерживаются.");
|
|
if (File.Exists(full) && (File.GetAttributes(full) & FileAttributes.ReparsePoint) != 0) throw new Exception("Файл комплекта заменён символической ссылкой.");
|
|
return full;
|
|
}
|
|
|
|
static string Hash(Stream stream)
|
|
{ using (var sha = SHA256.Create()) return BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", "").ToLowerInvariant(); }
|
|
|
|
static Dictionary<string, object> ObjectFile(string path)
|
|
{ return Json.Deserialize<Dictionary<string, object>>(File.ReadAllText(path, Encoding.UTF8)); }
|
|
|
|
#if SETUP
|
|
static void Install(string root, bool register, Action<int, string> report)
|
|
{
|
|
report(2, "Проверяю автономный комплект…");
|
|
string releases = Path.Combine(root, "releases"); Directory.CreateDirectory(releases);
|
|
string stage = Inside(releases, Version + "-" + Guid.NewGuid().ToString("N").Substring(0, 8)); Directory.CreateDirectory(stage);
|
|
string archive = Inside(stage, "payload.zip");
|
|
string release = null;
|
|
bool installed = false;
|
|
try
|
|
{
|
|
using (var input = Assembly.GetExecutingAssembly().GetManifestResourceStream("bundle.zip"))
|
|
using (var output = File.Create(archive)) input.CopyTo(output);
|
|
using (var input = File.OpenRead(archive)) if (Hash(input) != BundleHash) throw new Exception("Контрольная сумма комплекта не совпала.");
|
|
using (var zip = ZipFile.OpenRead(archive))
|
|
{
|
|
long total = zip.Entries.Sum(entry => entry.Length), written = 0;
|
|
if (total > 1024L * 1024 * 1024) throw new Exception("Некорректный размер комплекта.");
|
|
foreach (var entry in zip.Entries)
|
|
{
|
|
if (entry.FullName.EndsWith("/")) continue;
|
|
if (((entry.ExternalAttributes >> 16) & 0xF000) == 0xA000) throw new Exception("Символическая ссылка в архиве.");
|
|
string destination = Inside(stage, entry.FullName);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(destination));
|
|
using (var input = entry.Open()) using (var output = File.Create(destination)) input.CopyTo(output);
|
|
written += entry.Length;
|
|
report(5 + (int)(written * 70 / total), "Распаковываю компоненты…");
|
|
}
|
|
}
|
|
File.Delete(archive);
|
|
report(78, "Проверяю Python, загрузчик, JavaScript и FFmpeg…");
|
|
// Virus scanners can briefly hold a newly extracted EXE or DLL.
|
|
for (int attempt = 0; ; attempt++)
|
|
{
|
|
try { Python(stage, "verify"); break; }
|
|
catch { if (attempt == 2) throw; Thread.Sleep(700 * (attempt + 1)); }
|
|
}
|
|
release = stage;
|
|
Python(release, "configure");
|
|
string manager = Inside(root, "YouTubeDL-Manager.exe");
|
|
using (var input = Assembly.GetExecutingAssembly().GetManifestResourceStream("manager.exe"))
|
|
using (var output = File.Create(manager)) input.CopyTo(output);
|
|
string marker = Inside(root, "installation.json");
|
|
string history = Inside(root, @"data\completed-jobs.json");
|
|
string previousHistory = Inside(root, @".runtime\completed-jobs.json");
|
|
if (!File.Exists(history) && File.Exists(previousHistory))
|
|
{ Directory.CreateDirectory(Path.GetDirectoryName(history)); File.Copy(previousHistory, history); }
|
|
File.WriteAllText(marker, Json.Serialize(new { product = Product, version = Version, current = release, registered = register }), Encoding.UTF8);
|
|
string updates = Inside(root, @"data\update-settings.json");
|
|
if (File.Exists(updates))
|
|
{ var options = ObjectFile(updates); options["lastCheck"] = 0; File.WriteAllText(updates, Json.Serialize(options), new UTF8Encoding(false)); }
|
|
if (register)
|
|
{
|
|
report(92, "Подключаю помощник к Edge и Chrome…");
|
|
foreach (string browser in Browsers)
|
|
using (var registry = Registry.CurrentUser.CreateSubKey(@"Software\" + browser + @"\NativeMessagingHosts\" + Host))
|
|
registry.SetValue("", Path.Combine(release, @".runtime\native-host.json"));
|
|
using (var registry = Registry.CurrentUser.CreateSubKey(UninstallKey))
|
|
{
|
|
registry.SetValue("DisplayName", "YouTubeDL Helper"); registry.SetValue("DisplayVersion", Version);
|
|
registry.SetValue("Publisher", "YouTubeDL"); registry.SetValue("InstallLocation", root);
|
|
registry.SetValue("UninstallString", "\"" + manager + "\" --uninstall");
|
|
registry.SetValue("QuietUninstallString", "\"" + manager + "\" --uninstall --silent");
|
|
registry.SetValue("NoModify", 1); registry.SetValue("NoRepair", 1);
|
|
}
|
|
}
|
|
installed = true;
|
|
report(100, "Готово. Вернитесь в YouTubeDL и нажмите «Запустить».");
|
|
}
|
|
finally
|
|
{
|
|
// Only this freshly generated, unpublished folder may be removed.
|
|
// Cleanup must not hide the original validation/installation error.
|
|
try
|
|
{
|
|
if (!installed && Directory.Exists(stage) && (File.GetAttributes(stage) & FileAttributes.ReparsePoint) == 0)
|
|
{
|
|
var candidates = Directory.EnumerateFileSystemEntries(stage, "*", SearchOption.AllDirectories).ToArray();
|
|
if (candidates.All(path => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == 0)) Directory.Delete(stage, true);
|
|
}
|
|
}
|
|
catch (IOException) { } catch (UnauthorizedAccessException) { }
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
static void Activate(string root, string[] args, Action<int, string> report)
|
|
{
|
|
string marker = Inside(root, "installation.json");
|
|
var previous = ObjectFile(marker);
|
|
if ((string)previous["product"] != Product) throw new Exception("Установленный комплект не найден.");
|
|
string expected = PhysicalDirectory(Value(args, "--expected-current"));
|
|
string current = PhysicalDirectory((string)previous["current"]);
|
|
if (!String.Equals(current, expected, StringComparison.OrdinalIgnoreCase)) throw new Exception("Помощник уже изменён другой установкой. Повторите проверку обновлений.");
|
|
string stage = PhysicalDirectory(Path.GetFullPath(Value(args, "--activate-release")));
|
|
if (!String.Equals(Path.GetDirectoryName(stage), PhysicalDirectory(Inside(root, "releases")), StringComparison.OrdinalIgnoreCase)) throw new Exception("Папка обновления вне комплекта.");
|
|
var metadata = ObjectFile(Inside(stage, "bundle.json"));
|
|
if ((string)metadata["product"] != Product || (string)metadata["mode"] != "autonomous") throw new Exception("Некорректный комплект обновления.");
|
|
report(10, "Проверяю контрольные суммы обновления…");
|
|
foreach (var entry in ObjectFile(Inside(stage, "bundle-files.json")))
|
|
using (var input = File.OpenRead(Inside(stage, entry.Key)))
|
|
if (Hash(input) != (string)entry.Value) throw new Exception("Повреждён компонент обновления: " + entry.Key);
|
|
for (int attempt = 0; ; attempt++)
|
|
{ try { Python(stage, "verify"); break; } catch { if (attempt == 2) throw; Thread.Sleep(700 * (attempt + 1)); } }
|
|
Python(stage, "configure");
|
|
var registrations = new Dictionary<string, string>();
|
|
string nextMarker = Inside(root, "installation.next.json");
|
|
try
|
|
{
|
|
if ((bool)previous["registered"])
|
|
foreach (string browser in Browsers)
|
|
{
|
|
string name = @"Software\" + browser + @"\NativeMessagingHosts\" + Host;
|
|
using (var registry = Registry.CurrentUser.OpenSubKey(name, true))
|
|
{
|
|
string value = registry == null ? null : registry.GetValue("") as string;
|
|
if (value == null || !String.Equals(PhysicalDirectory(Path.GetDirectoryName(value)), PhysicalDirectory(Path.Combine(current, ".runtime")), StringComparison.OrdinalIgnoreCase)) continue;
|
|
registrations[name] = value; registry.SetValue("", Path.Combine(stage, @".runtime\native-host.json"));
|
|
}
|
|
}
|
|
File.WriteAllText(nextMarker, Json.Serialize(new { product = Product, version = (string)metadata["version"], current = stage, registered = (bool)previous["registered"] }), Encoding.UTF8);
|
|
File.Replace(nextMarker, marker, null);
|
|
}
|
|
catch
|
|
{
|
|
foreach (var entry in registrations) using (var registry = Registry.CurrentUser.CreateSubKey(entry.Key)) registry.SetValue("", entry.Value);
|
|
if (File.Exists(nextMarker)) File.Delete(nextMarker);
|
|
throw;
|
|
}
|
|
report(100, "Новые компоненты подключены.");
|
|
}
|
|
|
|
static void Python(string root, string action)
|
|
{
|
|
var start = new ProcessStartInfo(Inside(root, @"runtime\python\python.exe"), "-I -X utf8 -B -m host.bootstrap " + action)
|
|
{ UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true,
|
|
StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 };
|
|
start.EnvironmentVariables["PYTHONPATH"] = "C:\\NONEXISTENT"; start.EnvironmentVariables["PYTHONHOME"] = "C:\\NONEXISTENT";
|
|
start.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
|
|
start.EnvironmentVariables["PATH"] = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32");
|
|
using (var process = Process.Start(start))
|
|
{
|
|
var stdout = process.StandardOutput.ReadToEndAsync(); var stderr = process.StandardError.ReadToEndAsync();
|
|
if (!process.WaitForExit(90000)) throw new Exception("Проверка комплекта заняла слишком много времени.");
|
|
if (process.ExitCode != 0) throw new Exception("Комплект не прошёл проверку: " + stderr.Result + stdout.Result);
|
|
}
|
|
}
|
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
static extern Microsoft.Win32.SafeHandles.SafeFileHandle CreateFileW(string path, uint access, uint share, IntPtr security, uint disposition, uint flags, IntPtr template);
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
static extern uint GetFinalPathNameByHandleW(Microsoft.Win32.SafeHandles.SafeFileHandle file, StringBuilder name, uint length, uint flags);
|
|
static string PhysicalDirectory(string path)
|
|
{
|
|
using (var handle = CreateFileW(path, 0, 7, IntPtr.Zero, 3, 0x02000000, IntPtr.Zero))
|
|
{
|
|
if (handle.IsInvalid) return Path.GetFullPath(path);
|
|
var name = new StringBuilder(32768);
|
|
if (GetFinalPathNameByHandleW(handle, name, (uint)name.Capacity, 0) == 0) return Path.GetFullPath(path);
|
|
string value = name.ToString();
|
|
return value.StartsWith(@"\\?\UNC\") ? @"\\" + value.Substring(8) : value.StartsWith(@"\\?\") ? value.Substring(4) : value;
|
|
}
|
|
}
|
|
|
|
static void Uninstall(string root, Action<int, string> report)
|
|
{
|
|
string marker = Inside(root, "installation.json");
|
|
if (!File.Exists(marker) || (string)ObjectFile(marker)["product"] != Product) throw new Exception("Установленный комплект YouTubeDL не найден.");
|
|
string physicalRoot = PhysicalDirectory(root);
|
|
report(5, "Отключаю помощник…");
|
|
foreach (string browser in Browsers)
|
|
{
|
|
string name = @"Software\" + browser + @"\NativeMessagingHosts\" + Host;
|
|
using (var registry = Registry.CurrentUser.OpenSubKey(name))
|
|
{
|
|
string value = registry == null ? null : registry.GetValue("") as string;
|
|
if (value == null || !PhysicalDirectory(Path.GetDirectoryName(value)).StartsWith(physicalRoot + "\\releases\\", StringComparison.OrdinalIgnoreCase)) continue;
|
|
}
|
|
Registry.CurrentUser.DeleteSubKey(name, false);
|
|
}
|
|
using (var registry = Registry.CurrentUser.OpenSubKey(UninstallKey))
|
|
{ if (registry != null && registry.GetValue("InstallLocation") is string && String.Equals(PhysicalDirectory((string)registry.GetValue("InstallLocation")), physicalRoot, StringComparison.OrdinalIgnoreCase)) Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, false); }
|
|
string releases = Inside(root, "releases");
|
|
string[] packages = Directory.Exists(releases) ? Directory.GetDirectories(releases) : new string[0];
|
|
int done = 0;
|
|
foreach (string release in packages)
|
|
{
|
|
Inside(root, release.Substring(root.Length + 1) + "\\bundle.json");
|
|
if (!File.Exists(Path.Combine(release, "bundle.json")) || !File.Exists(Path.Combine(release, "bundle-files.json")) || (string)ObjectFile(Path.Combine(release, "bundle.json"))["product"] != Product) continue;
|
|
var records = ObjectFile(Path.Combine(release, "bundle-files.json"));
|
|
var directories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (string relative in records.Keys.Concat(new[] { "bundle-files.json", ".runtime/native-host.json", ".runtime/launch-host.cmd" }))
|
|
{
|
|
string path = Inside(release, relative);
|
|
if (File.Exists(path)) File.Delete(path);
|
|
for (string parent = Path.GetDirectoryName(path); parent != null && parent.StartsWith(release, StringComparison.OrdinalIgnoreCase); parent = Path.GetDirectoryName(parent)) directories.Add(parent);
|
|
}
|
|
foreach (string directory in directories.OrderByDescending(path => path.Length)) TryRemoveEmpty(directory);
|
|
report(10 + (++done * 80 / Math.Max(1, packages.Length)), "Удаляю встроенные компоненты…");
|
|
}
|
|
TryRemoveEmpty(releases);
|
|
string manager = Inside(root, "YouTubeDL-Manager.exe");
|
|
// Run the final cleanup outside the installed directory, after this EXE exits.
|
|
// The same mutex and an installation-marker check prevent a reinstall race.
|
|
if (File.Exists(manager))
|
|
{
|
|
string pending = Inside(root, "uninstall-pending.json");
|
|
File.WriteAllText(pending, Json.Serialize(new { product = Product }), Encoding.UTF8);
|
|
string cleanup = Path.Combine(Path.GetTempPath(), "YouTubeDL-Cleanup-" + Guid.NewGuid().ToString("N") + ".exe");
|
|
// Do not carry EFS attributes from AppData into the browser's temp
|
|
// directory; that directory may reside on a different volume.
|
|
File.WriteAllBytes(cleanup, File.ReadAllBytes(manager));
|
|
Process.Start(new ProcessStartInfo(cleanup, "--finish-uninstall --silent --target-root \"" + root + "\" --wait-pid " + Process.GetCurrentProcess().Id)
|
|
{ UseShellExecute = false, CreateNoWindow = true });
|
|
}
|
|
File.Delete(marker);
|
|
report(100, "Помощник удалён. Скачанные ролики сохранены.");
|
|
}
|
|
|
|
static void FinishRemoval(string root, Action<int, string> report)
|
|
{
|
|
string pending = Inside(root, "uninstall-pending.json");
|
|
if (!File.Exists(pending) || (string)ObjectFile(pending)["product"] != Product) return;
|
|
if (!File.Exists(Inside(root, "installation.json"))) File.Delete(Inside(root, "YouTubeDL-Manager.exe"));
|
|
File.Delete(pending); TryRemoveEmpty(root);
|
|
}
|
|
|
|
static void TryRemoveEmpty(string path)
|
|
{ try { if (Directory.Exists(path) && (File.GetAttributes(path) & FileAttributes.ReparsePoint) == 0) Directory.Delete(path, false); } catch (IOException) { } }
|
|
}
|
|
|
|
internal sealed class SetupWindow : Form
|
|
{
|
|
readonly Action<Action<int, string>> work;
|
|
readonly Label message;
|
|
readonly ProgressBar progress;
|
|
readonly Button close;
|
|
bool working = true;
|
|
public int ExitCode = 0;
|
|
public SetupWindow(Action<Action<int, string>> task, bool uninstall)
|
|
{
|
|
work = task; Text = "YouTubeDL"; Size = new Size(560, 285); StartPosition = FormStartPosition.CenterScreen;
|
|
FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; Font = new Font("Segoe UI", 10);
|
|
bool light = true;
|
|
using (var theme = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"))
|
|
light = theme == null || Convert.ToInt32(theme.GetValue("AppsUseLightTheme", 1)) != 0;
|
|
BackColor = light ? Color.FromArgb(248, 249, 251) : Color.FromArgb(22, 25, 31);
|
|
ForeColor = light ? Color.FromArgb(25, 30, 40) : Color.FromArgb(235, 241, 248);
|
|
var title = new Label { Text = uninstall ? "Удаление помощника" : "Установка помощника", Location = new Point(28, 24), AutoSize = true, Font = new Font("Segoe UI", 17, FontStyle.Bold) };
|
|
var subtitle = new Label { Text = "YouTubeDL · автономный комплект " + "@BUNDLE_VERSION@", Location = new Point(30, 65), AutoSize = true };
|
|
message = new Label { Text = "Подготавливаю…", Location = new Point(30, 105), Size = new Size(475, 47) };
|
|
progress = new ProgressBar { Location = new Point(30, 154), Size = new Size(480, 8), Style = ProgressBarStyle.Continuous };
|
|
close = new Button { Text = "Закрыть", Location = new Point(394, 189), Size = new Size(115, 33), Enabled = false, FlatStyle = FlatStyle.Flat };
|
|
close.Click += (sender, args) => Close(); Controls.AddRange(new Control[] { title, subtitle, message, progress, close });
|
|
FormClosing += (sender, args) => { if (working) args.Cancel = true; };
|
|
Shown += async (sender, args) =>
|
|
{
|
|
try
|
|
{
|
|
await Task.Run(() => work((percent, text) => BeginInvoke(new Action(() => { progress.Value = Math.Max(0, Math.Min(100, percent)); message.Text = text; }))));
|
|
}
|
|
catch (Exception error) { ExitCode = 1; message.Text = error.Message; }
|
|
finally { working = false; close.Enabled = true; close.Focus(); }
|
|
};
|
|
}
|
|
}
|