diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 3d3a0ba..14b9710 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,10 +3,11 @@ "isRoot": true, "tools": { "nuke.globaltool": { - "version": "5.0.2", + "version": "8.0.0", "commands": [ "nuke" - ] + ], + "rollForward": false } } } \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 1cd50c3..2f59d36 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,3 +5,85 @@ dotnet_analyzer_diagnostic.category-Globalization.severity = suggestion # CA1031: Do not catch general exception types dotnet_diagnostic.CA1031.severity = suggestion +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = file_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion diff --git a/.nuke b/.nuke deleted file mode 100644 index 14ef02a..0000000 --- a/.nuke +++ /dev/null @@ -1 +0,0 @@ -Excursion360.Desktop.sln \ No newline at end of file diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000..4dba4dc --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,115 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/build", + "title": "Build Schema", + "definitions": { + "build": { + "type": "object", + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", + "enum": [ + "Debug", + "Release" + ] + }, + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Runtime": { + "type": "string" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "Publish", + "Restore" + ] + } + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "Publish", + "Restore" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + } + } + } + } +} diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000..3e643f8 --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "Excursion360.Desktop.sln" +} diff --git a/Excursion360.Desktop/ConsoleHelper.cs b/Excursion360.Desktop/ConsoleHelper.cs index 252bb23..1d47715 100644 --- a/Excursion360.Desktop/ConsoleHelper.cs +++ b/Excursion360.Desktop/ConsoleHelper.cs @@ -1,42 +1,36 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +namespace Excursion360.Desktop; -namespace Excursion360.Desktop +public static class ConsoleHelper { - public static class ConsoleHelper + public static int SelectOneFromArray(string headerLine, string[] values) { - public static int SelectOneFromArray(string headerLine, string[] values) + values = values ?? throw new ArgumentNullException(nameof(values)); + var index = 0; + while (true) { - values = values ?? throw new ArgumentNullException(nameof(values)); - var index = 0; - while (true) + Console.Clear(); + Console.WriteLine(headerLine); + for (int i = 0; i < values.Length; i++) { - Console.Clear(); - Console.WriteLine(headerLine); - for (int i = 0; i < values.Length; i++) - { - Console.BackgroundColor = index == i ? ConsoleColor.White : ConsoleColor.Black; - Console.ForegroundColor = index == i ? ConsoleColor.Black : ConsoleColor.White; - Console.WriteLine(values[i]); - } - Console.BackgroundColor = ConsoleColor.Black; - Console.ForegroundColor = ConsoleColor.White; - var key = Console.ReadKey(); - switch (key.Key) - { - case ConsoleKey.UpArrow: - index = index <= 0 ? 0 : index - 1; - break; - case ConsoleKey.DownArrow: - index = index + 1 >= values.Length ? values.Length - 1 : index + 1; - break; - case ConsoleKey.Enter: - return index; - default: - break; - } + Console.BackgroundColor = index == i ? ConsoleColor.White : ConsoleColor.Black; + Console.ForegroundColor = index == i ? ConsoleColor.Black : ConsoleColor.White; + Console.WriteLine(values[i]); + } + Console.BackgroundColor = ConsoleColor.Black; + Console.ForegroundColor = ConsoleColor.White; + var key = Console.ReadKey(); + switch (key.Key) + { + case ConsoleKey.UpArrow: + index = index <= 0 ? 0 : index - 1; + break; + case ConsoleKey.DownArrow: + index = index + 1 >= values.Length ? values.Length - 1 : index + 1; + break; + case ConsoleKey.Enter: + return index; + default: + break; } } } diff --git a/Excursion360.Desktop/Excursion360.Desktop.csproj b/Excursion360.Desktop/Excursion360.Desktop.csproj index 97b7e44..ea70768 100644 --- a/Excursion360.Desktop/Excursion360.Desktop.csproj +++ b/Excursion360.Desktop/Excursion360.Desktop.csproj @@ -2,8 +2,10 @@ Exe - netcoreapp3.1 + net8.0-windows true + enable + enable @@ -20,13 +22,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/Excursion360.Desktop/Extensions.cs b/Excursion360.Desktop/Extensions.cs index 695d548..6c8a0e3 100644 --- a/Excursion360.Desktop/Extensions.cs +++ b/Excursion360.Desktop/Extensions.cs @@ -1,41 +1,23 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.Extensions.Hosting; -using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; -using System.Linq; +using Microsoft.AspNetCore.Http.Features; -namespace Excursion360.Desktop +namespace Excursion360.Desktop; + +static class Extensions { - static class Extensions + public static ILogger CreateLogger(this IWebHost host, string categoryName) + => host.Services.GetRequiredService().CreateLogger(categoryName); + public static Uri GetListeningUri(this IHost host) { - public static ILogger CreateLogger(this IWebHost host, string categoryName) - { - return host.Services.GetService().CreateLogger(categoryName); - } - public static Uri GetListeningUri(this IHost host) - { - return new Uri(host.Services - .GetRequiredService() - .Features - .Get() - .Addresses - .Single(a => a.StartsWith("http:", StringComparison.Ordinal))); - } - public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector) - { - HashSet seenKeys = new HashSet(); - foreach (TSource element in source) - { - if (seenKeys.Add(keySelector(element))) - { - yield return element; - } - } - } + return new Uri(host.Services + .GetRequiredService() + .Features + .GetRequiredFeature() + .Addresses + .Single(a => a.StartsWith("http:", StringComparison.Ordinal))); } + + public static string ExcursionDirectoryPath(this IConfiguration configuration) + => configuration.GetValue("excursionsPath", null) ?? Directory.GetCurrentDirectory(); } diff --git a/Excursion360.Desktop/Program.cs b/Excursion360.Desktop/Program.cs index 4681511..54bc4f7 100644 --- a/Excursion360.Desktop/Program.cs +++ b/Excursion360.Desktop/Program.cs @@ -1,175 +1,114 @@ -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.DependencyInjection; -using System; -using Microsoft.Extensions.Configuration; -using Microsoft.AspNetCore.Hosting.Server.Features; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Win32; -using System.IO; -using System.Diagnostics; -using System.Threading; -using System.Net.Http; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using Excursion360.Desktop.Services.Firefox; -using Microsoft.Extensions.Hosting; -using Microsoft.AspNetCore.Hosting.Server; -using System.Net.NetworkInformation; using Microsoft.Extensions.FileProviders; using Excursion360.Desktop.Services; using System.Reflection; using Excursion360.Desktop.Exceptions; using MintPlayer.PlatformBrowser; -using System.Collections.Generic; +using Excursion360.Desktop; -namespace Excursion360.Desktop +Console.BackgroundColor = ConsoleColor.Black; +Console.ForegroundColor = ConsoleColor.White; + +var builder = WebApplication.CreateBuilder(args); +builder.Logging.SetMinimumLevel(LogLevel.Information); +builder.Services.AddSingleton(); +builder.Services.AddResponseCompression(); +if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +{ + builder.Services.AddHttpClient(); +} +else { - class Program + builder.Services.AddHttpClient(); +} + +var app = builder.Build(); +var targetDirectory = GetExcursionDirectory(builder.Configuration.ExcursionDirectoryPath()); + +app.MapGet("/eapi/preload.json", (IStateImagesMetricsStore stateImagesMetrics) => { + return new { - static async Task Main(string[] args) - { - Console.BackgroundColor = ConsoleColor.Black; - Console.ForegroundColor = ConsoleColor.White; - - - try - { - var targetDirectory = GetExcursionDirectory(); - using IHost host = CreateHost(args, targetDirectory); - IBrowser browser = await SelectBrowser(args, targetDirectory, host).ConfigureAwait(false); - var hostTask = host.RunAsync().ConfigureAwait(false); - await browser.StartBrowser(host.GetListeningUri()); - await hostTask; - } - catch (IncorrectEnvironmentException ex) - { - Console.WriteLine(ex.Message); - } - catch (Exception ex) - { - Console.WriteLine($"Unexpected error, please tell us about that https://github.com/RTUITLab/Excursion360-Desktop/issues"); - Console.WriteLine(ex.Message); - Console.WriteLine(ex.StackTrace); - } - Console.WriteLine("Press any key to exit"); - Console.ReadKey(); - } + images = stateImagesMetrics.MostPopularUrls(), + }; +}); - private static async Task SelectBrowser(string[] args, string targetDirectory, IHost host) - { - bool useFirefox = SelectFirefoxOrInstalled(args, targetDirectory, out var selectedBrowser); - return useFirefox ? - await SetupFirefox(host).ConfigureAwait(false) - : - new GenericBrowser(selectedBrowser); - } +var fso = new FileServerOptions +{ + FileProvider = new CompositeFileProvider( + new PhysicalFileProvider(Path.Combine(builder.Configuration.ExcursionDirectoryPath(), targetDirectory)), + new EmbeddedFileProvider(Assembly.GetExecutingAssembly())) +}; +fso.DefaultFilesOptions.DefaultFileNames.Add("Resources/NotFound.html"); +fso.StaticFileOptions.OnPrepareResponse = (context) => +{ + if (context.File.Name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) && context.File.PhysicalPath is not null) + { + var stateImagesMetricsStore = context.Context.RequestServices.GetRequiredService(); + stateImagesMetricsStore.IncrementImageHit(context.Context.Request.Path); + } +}; +app.UseResponseCompression(); +app.UseFileServer(fso); - private static string GetExcursionDirectory() - { - var dirs = Directory.GetDirectories(Directory.GetCurrentDirectory()).Select(d => Path.GetFileName(d)).ToArray(); - if (dirs.Length == 0) - { - throw new IncorrectEnvironmentException("You must place excursion files to the subdirectory with executable file"); - } - if (dirs.Length == 1) - { - return dirs[0]; - } - return dirs[ConsoleHelper.SelectOneFromArray("Select directory with excursion", dirs)]; - } - private static async Task SetupFirefox(IHost host) - { - var firefoxInterop = host.Services.GetRequiredService(); - if (!await firefoxInterop.IsFirefoxInstalled()) - { - if (!await firefoxInterop.TryInstallFirefoxAsync().ConfigureAwait(false)) - { - throw new IncorrectEnvironmentException("Can't install firefox"); - } - } - return firefoxInterop; - } +IBrowser browser = await SelectBrowser(targetDirectory, app.Services, app.Configuration).ConfigureAwait(false); + +var hostTask = app.RunAsync(); +await browser.StartBrowser(app.GetListeningUri()); +await hostTask; - /// - /// - /// - /// - /// - /// true if need firefox, false if selected another browser - private static bool SelectFirefoxOrInstalled(string[] args, string targetDirectory, out Browser browser) - { - browser = null; - if (args.Contains("--firefox")) - { - return true; - } - var browsers = PlatformBrowser - .GetInstalledBrowsers() - .Where(b => !b.Name.Contains("Explorer")) - .Where(b => !b.Name.Contains("Firefox")) - .DistinctBy(b => b.Name) - .ToArray(); - var browsersList = new List { "Use Firefox (install if not present)" }; - browsersList.AddRange(browsers.Select(b => b.Name)); - - var browserMode = ConsoleHelper.SelectOneFromArray($"Select run option. Selected excursion: {targetDirectory}", browsersList.ToArray()); - Console.Clear(); - if (browserMode == 0) // Use furefox - { - return true; - } - else - { - browser = browsers[browserMode - 1]; - return false; - } - } - private static IHost CreateHost(string[] args, string targetDirectory) +async Task SelectBrowser(string targetDirectory, IServiceProvider serviceProvider, IConfiguration configuration) +{ + if (configuration.GetValue("--firefox")) + { + return await SetupFirefox(serviceProvider).ConfigureAwait(false); + } + var selectedBrowser = await SelectInstalledBrowserAsync(targetDirectory); + return new GenericBrowser(selectedBrowser); +} + +string GetExcursionDirectory(string rootDir) +{ + var dirs = Directory.GetDirectories(rootDir).Select(d => Path.GetFileName(d)).ToArray(); + if (dirs.Length == 0) + { + throw new IncorrectEnvironmentException("You must place excursion files to the subdirectory with executable file"); + } + if (dirs.Length == 1) + { + return dirs[0]; + } + return dirs[ConsoleHelper.SelectOneFromArray("Select directory with excursion", dirs)]; +} + +async Task SetupFirefox(IServiceProvider serviceProvider) +{ + var firefoxInterop = serviceProvider.GetRequiredService(); + if (!await firefoxInterop.IsFirefoxInstalled()) + { + if (!await firefoxInterop.TryInstallFirefoxAsync().ConfigureAwait(false)) { - var host = Host - .CreateDefaultBuilder(args) - .ConfigureLogging(logs => - { - logs.SetMinimumLevel(LogLevel.Information); - }) - .ConfigureWebHostDefaults(webBuilder => - { - var fso = new FileServerOptions - { - FileProvider = new CompositeFileProvider( - new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), targetDirectory)), - new EmbeddedFileProvider(Assembly.GetExecutingAssembly())) - }; - fso.DefaultFilesOptions.DefaultFileNames.Add("Resources/NotFound.html"); - fso.StaticFileOptions.OnPrepareResponse = (context) => - { - context.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store"); - context.Context.Response.Headers.Add("Expires", "-1"); - }; - webBuilder.Configure(app => - { - app.UseFileServer(fso); - }); - }) - .ConfigureServices(services => - { - services.AddHttpClient(nameof(IFirefoxInterop)); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - services.AddSingleton(); - }// TODO Add other OS - else - { - services.AddSingleton(); - } - }) - .Build(); - return host; + throw new IncorrectEnvironmentException("Can't install firefox"); } } + return firefoxInterop; +} + + +async Task SelectInstalledBrowserAsync(string targetDirectory) +{ + var browsers = (await PlatformBrowser + .GetInstalledBrowsers()) + .Where(b => !b.Name.Contains("Explorer")) + .DistinctBy(b => b.Name) + .ToArray(); + + var browsersList = new List { "Use Firefox (install if not present)" }; + browsersList.AddRange(browsers.Select(b => b.Name)); + + var browserMode = ConsoleHelper.SelectOneFromArray($"Select run option. Selected excursion: {targetDirectory}", [.. browsersList]); + Console.Clear(); + return browsers[browserMode - 1]; } diff --git a/Excursion360.Desktop/Services/Firefox/IFirefoxInterop.cs b/Excursion360.Desktop/Services/Firefox/IFirefoxInterop.cs index 7c2a491..53dae6c 100644 --- a/Excursion360.Desktop/Services/Firefox/IFirefoxInterop.cs +++ b/Excursion360.Desktop/Services/Firefox/IFirefoxInterop.cs @@ -1,13 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; +namespace Excursion360.Desktop.Services.Firefox; -namespace Excursion360.Desktop.Services.Firefox +public interface IFirefoxInterop : IBrowser { - public interface IFirefoxInterop : IBrowser - { - Task TryInstallFirefoxAsync(); - ValueTask IsFirefoxInstalled(); - } + Task TryInstallFirefoxAsync(); + ValueTask IsFirefoxInstalled(); } diff --git a/Excursion360.Desktop/Services/Firefox/UnsupportedOsFirefoxInterop.cs b/Excursion360.Desktop/Services/Firefox/UnsupportedOsFirefoxInterop.cs index 459b999..01888c5 100644 --- a/Excursion360.Desktop/Services/Firefox/UnsupportedOsFirefoxInterop.cs +++ b/Excursion360.Desktop/Services/Firefox/UnsupportedOsFirefoxInterop.cs @@ -1,24 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -namespace Excursion360.Desktop.Services.Firefox +namespace Excursion360.Desktop.Services.Firefox; + +public class UnsupportedOsFirefoxInterop : IFirefoxInterop { - public class UnsupportedOsFirefoxInterop : IFirefoxInterop + public ValueTask IsFirefoxInstalled() { - public ValueTask IsFirefoxInstalled() - { - throw new NotSupportedException(); - } + throw new NotSupportedException(); + } - public ValueTask StartBrowser(Uri uri) - { - throw new NotSupportedException(); - } + public ValueTask StartBrowser(Uri uri) + { + throw new NotSupportedException(); + } - public Task TryInstallFirefoxAsync() - { - throw new NotSupportedException(); - } + public Task TryInstallFirefoxAsync() + { + throw new NotSupportedException(); } } diff --git a/Excursion360.Desktop/Services/Firefox/WindowsFirefoxInterop.cs b/Excursion360.Desktop/Services/Firefox/WindowsFirefoxInterop.cs index 96c2bfe..7c6057b 100644 --- a/Excursion360.Desktop/Services/Firefox/WindowsFirefoxInterop.cs +++ b/Excursion360.Desktop/Services/Firefox/WindowsFirefoxInterop.cs @@ -1,31 +1,14 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Win32; -using System; +using Microsoft.Win32; using System.Buffers; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; namespace Excursion360.Desktop.Services.Firefox { - public class WindowsFirefoxInterop : IFirefoxInterop + public class WindowsFirefoxInterop(HttpClient httpClient, ILogger logger) : IFirefoxInterop { private const string FirefosinstallerUri = "https://download-installer.cdn.mozilla.net/pub/firefox/releases/71.0/win64/ru/Firefox%20Setup%2071.0.msi"; private const string RegistryFirefoxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\firefox.exe"; - private readonly ILogger logger; - private readonly HttpClient httpClient; - public WindowsFirefoxInterop( - ILogger logger, - IHttpClientFactory httpClientFactory) - { - this.logger = logger; - httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); - httpClient = httpClientFactory.CreateClient(nameof(IFirefoxInterop)); - } public async Task TryInstallFirefoxAsync() { logger.LogInformation("Installing firefox..."); @@ -33,11 +16,11 @@ public async Task TryInstallFirefoxAsync() string installerFilePath = Path.Combine(Directory.GetCurrentDirectory() + @"\firefox.msi"); Uri downloadUri = new Uri(FirefosinstallerUri); - using (HttpResponseMessage response = httpClient.GetAsync(downloadUri, HttpCompletionOption.ResponseHeadersRead).Result) + using (var response = await httpClient.GetAsync(downloadUri, HttpCompletionOption.ResponseHeadersRead)) { if (!response.IsSuccessStatusCode) { - logger.LogInformation($"The request returned with HTTP status code {response.StatusCode}"); + logger.LogInformation("The request returned with HTTP status code {ResponseStatusCode}", response.StatusCode); return false; } @@ -50,14 +33,14 @@ public async Task TryInstallFirefoxAsync() do { - var read = await contentStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + var read = await contentStream.ReadAsync(buffer).ConfigureAwait(false); if (read == 0) { isMoreToRead = false; } else { - await fileStream.WriteAsync(buffer, 0, read).ConfigureAwait(false); + await fileStream.WriteAsync(buffer.AsMemory(0, read)).ConfigureAwait(false); totalRead += read; totalReads += 1; @@ -68,7 +51,7 @@ public async Task TryInstallFirefoxAsync() ArrayPool.Shared.Return(buffer); } - using Process installerProcess = new Process + using var installerProcess = new Process { StartInfo = new ProcessStartInfo("cmd.exe") { @@ -97,7 +80,7 @@ public ValueTask IsFirefoxInstalled() { logger.LogInformation("Checking for firefox..."); - object path = Registry.GetValue(RegistryFirefoxKey, "", null); + object? path = Registry.GetValue(RegistryFirefoxKey, "", null); if (path != null) { logger.LogInformation("Firefox installed"); } return new ValueTask(path != null); @@ -106,9 +89,10 @@ public ValueTask IsFirefoxInstalled() public ValueTask StartBrowser(Uri uri) { uri = uri ?? throw new ArgumentNullException(nameof(uri)); - logger.LogInformation($"Starting firefox on {uri.AbsoluteUri}"); - - Process.Start(Registry.GetValue(RegistryFirefoxKey, "", null).ToString(), uri.AbsoluteUri); + logger.LogInformation("Starting firefox on {StartUrl}", uri.AbsoluteUri); + var firefoxPath = Registry.GetValue(RegistryFirefoxKey, "", null)?.ToString() + ?? throw new InvalidDataException($"Can't run firefox, not found registry key value {RegistryFirefoxKey}"); + Process.Start(firefoxPath, uri.AbsoluteUri); return new ValueTask(); } } diff --git a/Excursion360.Desktop/Services/GenericBrowser.cs b/Excursion360.Desktop/Services/GenericBrowser.cs index 6dedbd9..9608af2 100644 --- a/Excursion360.Desktop/Services/GenericBrowser.cs +++ b/Excursion360.Desktop/Services/GenericBrowser.cs @@ -1,26 +1,14 @@ using MintPlayer.PlatformBrowser; -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -namespace Excursion360.Desktop.Services +namespace Excursion360.Desktop.Services; + +public class GenericBrowser(Browser browser) : IBrowser { - public class GenericBrowser : IBrowser + public ValueTask StartBrowser(Uri uri) { - private readonly Browser browser; - - public GenericBrowser(Browser browser) - { - this.browser = browser; - } - public ValueTask StartBrowser(Uri uri) - { - uri = uri ?? throw new ArgumentNullException(nameof(uri)); - Process.Start(browser.ExecutablePath, uri.AbsoluteUri); - return new ValueTask(); - } + uri = uri ?? throw new ArgumentNullException(nameof(uri)); + Process.Start(browser.ExecutablePath, uri.AbsoluteUri); + return new ValueTask(); } } diff --git a/Excursion360.Desktop/Services/IBrowser.cs b/Excursion360.Desktop/Services/IBrowser.cs index 3567271..9e983aa 100644 --- a/Excursion360.Desktop/Services/IBrowser.cs +++ b/Excursion360.Desktop/Services/IBrowser.cs @@ -1,10 +1,6 @@ -using System; -using System.Threading.Tasks; +namespace Excursion360.Desktop.Services; -namespace Excursion360.Desktop.Services +public interface IBrowser { - public interface IBrowser - { - ValueTask StartBrowser(Uri uri); - } + ValueTask StartBrowser(Uri uri); } \ No newline at end of file diff --git a/Excursion360.Desktop/Services/IStateImagesMetricsStore.cs b/Excursion360.Desktop/Services/IStateImagesMetricsStore.cs new file mode 100644 index 0000000..ea6a1c6 --- /dev/null +++ b/Excursion360.Desktop/Services/IStateImagesMetricsStore.cs @@ -0,0 +1,47 @@ +using System.Collections.Concurrent; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Excursion360.Desktop.Services; + +public interface IStateImagesMetricsStore +{ + void IncrementImageHit(string path); + string[] MostPopularUrls(); +} + +public partial class InMemoryIStateImagesMetricsStore : IStateImagesMetricsStore +{ + /// + /// Количества получений картинок по адресу. ключ = адрес картинки. Значение - количество получений. + /// + private readonly ConcurrentDictionary + hitCounts = new(); + private Regex stateAndImageRegex = GetStateAndImageRegex(); + public void IncrementImageHit(string path) + { + var match = stateAndImageRegex.Match(path); + if (!match.Success) + { + return; + } + hitCounts.AddOrUpdate(path, 1, (_, old) => old + 1); + Console.WriteLine(JsonSerializer.Serialize(hitCounts, new JsonSerializerOptions + { + WriteIndented = true, + })); + } + public string[] MostPopularUrls() + { + return hitCounts + .OrderByDescending(kvp => kvp.Value) + .ThenBy(kvp => kvp.Key) + .Select(kvp => kvp.Key) + .Take(10) + .ToArray(); + } + + [GeneratedRegex(@"state_.+[/\\].+\.jpg$")] + private static partial Regex GetStateAndImageRegex(); + +} diff --git a/README.md b/README.md index 08d9022..e543713 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Desktop viewer for 360 excursions ## CLI Options -Run executable file with `--firefox` to force using firefox instead selecting on start. +Run executable file with `--firefox true` to force using firefox instead selecting on start. ## Build diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 24e213d..f7b1433 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1 +1,8 @@ -# Возможность выбирать используемый браузер из установленных на ПК +- `v5.0` + - Добавлено сжатие ресурсов при отправке клиенту +- `v4.0` + - Обновлен .NET/Nuke до 8, убраны все предупреждения + - Обновлена структура проекта до последней версии ASP.NET Core + - Базовая папка для выбора экскурсий может выбираться через `--excursionsPath путь_к_папке` + - Добавлена обработка `/eapi/preload.json`, по которому будут отдаваться адреса изображений, которые чаще всего просматриваются +- `v3.0` Возможность выбирать используемый браузер из установленных на ПК diff --git a/build.cmd b/build.cmd old mode 100644 new mode 100755 index 8b8b89d..b08cc59 --- a/build.cmd +++ b/build.cmd @@ -4,4 +4,4 @@ :; exit $? @ECHO OFF -powershell -ExecutionPolicy ByPass -NoProfile "%~dp0build.ps1" %* +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 index e5c8a44..4634dc0 100644 --- a/build.ps1 +++ b/build.ps1 @@ -14,15 +14,14 @@ $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent ########################################################################### $BuildProjectFile = "$PSScriptRoot\build\_build.csproj" -$TempDirectory = "$PSScriptRoot\\.tmp" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" $DotNetGlobalFile = "$PSScriptRoot\\global.json" $DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" -$DotNetChannel = "Current" +$DotNetChannel = "STS" -$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 -$env:DOTNET_MULTILEVEL_LOOKUP = 0 +$env:DOTNET_NOLOGO = 1 ########################################################################### # EXECUTION @@ -56,14 +55,20 @@ else { # Install by channel or version $DotNetDirectory = "$TempDirectory\dotnet-win" if (!(Test-Path variable:DotNetVersion)) { - ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } } else { - ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + $env:PATH = "$DotNetDirectory;$env:PATH" } -Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" + +if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { + & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null + & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null +} ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index 3d52643..fdff0c6 --- a/build.sh +++ b/build.sh @@ -10,15 +10,14 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) ########################################################################### BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" -TEMP_DIRECTORY="$SCRIPT_DIR//.tmp" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" -DOTNET_CHANNEL="Current" +DOTNET_CHANNEL="STS" export DOTNET_CLI_TELEMETRY_OPTOUT=1 -export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 -export DOTNET_MULTILEVEL_LOOKUP=0 +export DOTNET_NOLOGO=1 ########################################################################### # EXECUTION @@ -54,9 +53,15 @@ else "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path fi export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + export PATH="$DOTNET_DIRECTORY:$PATH" fi -echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + +if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then + "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true + "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true +fi "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/build/Build.cs b/build/Build.cs index 7ab99d2..d11e367 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -15,7 +15,6 @@ using static Nuke.Common.Tools.DotNet.DotNetTasks; using static Nuke.Common.Logger; -[CheckBuildProjectConfigurations] [ShutdownDotNetAfterServerBuild] class Build : NukeBuild { @@ -41,7 +40,7 @@ class Build : NukeBuild .Before(Restore) .Executes(() => { - EnsureCleanDirectory(OutputDirectory); + OutputDirectory.CreateOrCleanDirectory(); }); Target Restore => _ => _ @@ -77,6 +76,7 @@ class Build : NukeBuild .EnablePublishTrimmed() .SetProperty("DebugType", "None") .SetProperty("DebugSymbols", false) + .SetProperty("PublishIISAssets", false) .EnableNoRestore()); var executableFile = System.IO.Directory.GetFiles(OutputDirectory) diff --git a/build/_build.csproj b/build/_build.csproj index 07fcbe0..5331a66 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -2,15 +2,16 @@ Exe - netcoreapp3.1 + net8.0 CS0649;CS0169 .. .. + 1 - + diff --git a/global.json b/global.json new file mode 100644 index 0000000..26d18ee --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "minor" + } +}