diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index a994ff33..bc1d4780 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -16,7 +16,7 @@ namespace Microsoft.Extensions.DependencyInjection return services; } - internal static IServiceCollection AddOqtaneScopedServices(this IServiceCollection services) + public static IServiceCollection AddOqtaneScopedServices(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 14916d80..0828d9ce 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -184,11 +184,12 @@ var interop = new Interop(JSRuntime); if (await interop.FormValid(login)) { + var hybrid = (PageState.Runtime == Shared.Runtime.Hybrid); var user = new User { SiteId = PageState.Site.SiteId, Username = _username, Password = _password, LastIPAddress = SiteState.RemoteIPAddress}; - + if (!twofactor) { - user = await UserService.LoginUserAsync(user); + user = await UserService.LoginUserAsync(user, hybrid, _remember); } else { @@ -199,10 +200,20 @@ { await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username); - // post back to the Login page so that the cookies are set correctly - var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl }; - string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/"); - await interop.SubmitForm(url, fields); + if (hybrid) + { + // hybrid apps utilize an interactive login + var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider)); + authstateprovider.NotifyAuthenticationChanged(); + NavigationManager.NavigateTo(NavigateUrl(_returnUrl, true)); + } + else + { + // post back to the Login page so that the cookies are set correctly + var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = _returnUrl }; + string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/"); + await interop.SubmitForm(url, fields); + } } else { diff --git a/Oqtane.Client/Modules/Admin/Sites/Add.razor b/Oqtane.Client/Modules/Admin/Sites/Add.razor index f1ac13fe..76967b40 100644 --- a/Oqtane.Client/Modules/Admin/Sites/Add.razor +++ b/Oqtane.Client/Modules/Admin/Sites/Add.razor @@ -336,7 +336,7 @@ else user.Username = _hostusername; user.Password = _hostpassword; user.LastIPAddress = PageState.RemoteIPAddress; - user = await UserService.LoginUserAsync(user); + user = await UserService.LoginUserAsync(user, false, false); if (user.IsAuthenticated) { var database = _databases.SingleOrDefault(d => d.Name == _databaseName); diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index b87c6a2c..13f7fe81 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -55,7 +55,8 @@ namespace Oqtane.Modules var scripts = new List(); foreach (Resource resource in Resources.Where(item => item.ResourceType == ResourceType.Script)) { - scripts.Add(new { href = resource.Url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module }); + var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + "/" + resource.Url; + scripts.Add(new { href = url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module }); } if (scripts.Any()) { diff --git a/Oqtane.Client/Program.cs b/Oqtane.Client/Program.cs index 0a327c7a..40ed3917 100644 --- a/Oqtane.Client/Program.cs +++ b/Oqtane.Client/Program.cs @@ -33,7 +33,7 @@ namespace Oqtane.Client builder.Services.AddOptions(); - // Register localization services + // register localization services builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); // register auth services diff --git a/Oqtane.Client/Services/Interfaces/IUserService.cs b/Oqtane.Client/Services/Interfaces/IUserService.cs index 012c5f79..00102a5e 100644 --- a/Oqtane.Client/Services/Interfaces/IUserService.cs +++ b/Oqtane.Client/Services/Interfaces/IUserService.cs @@ -54,8 +54,10 @@ namespace Oqtane.Services /// Note that this will probably not be a real User, but a user object where the `Username` and `Password` have been filled. /// /// A object which should have at least the and set. + /// Determines if the login cookie should be set (only relevant for Hybrid scenarios) + /// Determines if the login cookie should be persisted for a long time. /// - Task LoginUserAsync(User user); + Task LoginUserAsync(User user, bool setCookie, bool isPersistent); /// /// Logout a diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 7d363e07..24440df5 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -39,9 +39,9 @@ namespace Oqtane.Services await DeleteAsync($"{Apiurl}/{userId}?siteid={siteId}"); } - public async Task LoginUserAsync(User user) + public async Task LoginUserAsync(User user, bool setCookie, bool isPersistent) { - return await PostJsonAsync($"{Apiurl}/login", user); + return await PostJsonAsync($"{Apiurl}/login?setcookie={setCookie}&persistent={isPersistent}", user); } public async Task LogoutUserAsync(User user) diff --git a/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs b/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs index 6965b845..facebb15 100644 --- a/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs +++ b/Oqtane.Client/Themes/Controls/Theme/LoginBase.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; using Oqtane.Enums; +using Oqtane.Providers; using Oqtane.Security; using Oqtane.Services; using Oqtane.Shared; @@ -38,12 +39,23 @@ namespace Oqtane.Themes.Controls if (!UserSecurity.IsAuthorized(null, PermissionNames.View, PageState.Page.Permissions)) { url = PageState.Alias.Path; - } + } - // post to the Logout page to complete the logout process - var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url }; - var interop = new Interop(jsRuntime); - await interop.SubmitForm(Utilities.TenantUrl(PageState.Alias, "/pages/logout/"), fields); + if (PageState.Runtime == Shared.Runtime.Hybrid) + { + // hybrid apps utilize an interactive logout + await UserService.LogoutUserAsync(PageState.User); + var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider)); + authstateprovider.NotifyAuthenticationChanged(); + NavigationManager.NavigateTo(url, true); + } + else + { + // post to the Logout page to complete the logout process + var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url }; + var interop = new Interop(jsRuntime); + await interop.SubmitForm(Utilities.TenantUrl(PageState.Alias, "/pages/logout/"), fields); + } } } } diff --git a/Oqtane.Client/UI/ThemeBuilder.razor b/Oqtane.Client/UI/ThemeBuilder.razor index 449cc4ca..1db0d301 100644 --- a/Oqtane.Client/UI/ThemeBuilder.razor +++ b/Oqtane.Client/UI/ThemeBuilder.razor @@ -36,7 +36,8 @@ foreach (Resource resource in PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Stylesheet)) { var prefix = "app-stylesheet-" + resource.Level.ToString().ToLower(); - links.Add(new { id = prefix + "-" + batch + "-" + (links.Count + 1).ToString("00"), rel = "stylesheet", href = resource.Url, type = "text/css", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", insertbefore = prefix }); + var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + "/" + resource.Url; + links.Add(new { id = prefix + "-" + batch + "-" + (links.Count + 1).ToString("00"), rel = "stylesheet", href = url, type = "text/css", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", insertbefore = prefix }); } if (links.Any()) { diff --git a/Oqtane.Maui.sln b/Oqtane.Maui.sln new file mode 100644 index 00000000..1082255b --- /dev/null +++ b/Oqtane.Maui.sln @@ -0,0 +1,32 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31611.283 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Maui", "Oqtane.Maui\Oqtane.Maui.csproj", "{5EE64148-2152-4908-A3E7-658EB1D87754}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Server", "Oqtane.Server\Oqtane.Server.csproj", "{62D43CB1-6CFB-4C4D-B6F7-DB610CE23188}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5EE64148-2152-4908-A3E7-658EB1D87754}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EE64148-2152-4908-A3E7-658EB1D87754}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EE64148-2152-4908-A3E7-658EB1D87754}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {5EE64148-2152-4908-A3E7-658EB1D87754}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EE64148-2152-4908-A3E7-658EB1D87754}.Release|Any CPU.Build.0 = Release|Any CPU + {5EE64148-2152-4908-A3E7-658EB1D87754}.Release|Any CPU.Deploy.0 = Release|Any CPU + {62D43CB1-6CFB-4C4D-B6F7-DB610CE23188}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62D43CB1-6CFB-4C4D-B6F7-DB610CE23188}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62D43CB1-6CFB-4C4D-B6F7-DB610CE23188}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62D43CB1-6CFB-4C4D-B6F7-DB610CE23188}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {61F7FB11-1E47-470C-91E2-47F8143E1572} + EndGlobalSection +EndGlobal diff --git a/Oqtane.Maui/App.xaml b/Oqtane.Maui/App.xaml new file mode 100644 index 00000000..7d992367 --- /dev/null +++ b/Oqtane.Maui/App.xaml @@ -0,0 +1,26 @@ + + + + + + #512bdf + White + + + + + + + + \ No newline at end of file diff --git a/Oqtane.Maui/App.xaml.cs b/Oqtane.Maui/App.xaml.cs new file mode 100644 index 00000000..0d60ea85 --- /dev/null +++ b/Oqtane.Maui/App.xaml.cs @@ -0,0 +1,11 @@ +namespace Oqtane.Maui; + +public partial class App : Application +{ + public App() + { + InitializeComponent(); + + MainPage = new MainPage(); + } +} diff --git a/Oqtane.Maui/Main.razor b/Oqtane.Maui/Main.razor new file mode 100644 index 00000000..793eb8c9 --- /dev/null +++ b/Oqtane.Maui/Main.razor @@ -0,0 +1,26 @@ +@inherits ErrorBoundary + + + +@code { + Type ComponentType = Type.GetType("Oqtane.App, Oqtane.Client"); + private IDictionary Parameters { get; set; } + + protected override void OnInitialized() + { + Parameters = new Dictionary(); + Parameters.Add(new KeyValuePair("AntiForgeryToken", "")); + Parameters.Add(new KeyValuePair("Runtime", "Hybrid")); + Parameters.Add(new KeyValuePair("RenderMode", "Hybrid")); + Parameters.Add(new KeyValuePair("VisitorId", -1)); + Parameters.Add(new KeyValuePair("RemoteIPAddress", "")); + Parameters.Add(new KeyValuePair("AuthorizationToken", "")); + } + + protected override async Task OnErrorAsync(Exception exception) + { + await base.OnErrorAsync(exception); + return; + } +} + diff --git a/Oqtane.Maui/MainPage.xaml b/Oqtane.Maui/MainPage.xaml new file mode 100644 index 00000000..e56e1dbf --- /dev/null +++ b/Oqtane.Maui/MainPage.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Oqtane.Maui/MainPage.xaml.cs b/Oqtane.Maui/MainPage.xaml.cs new file mode 100644 index 00000000..fc8e97d1 --- /dev/null +++ b/Oqtane.Maui/MainPage.xaml.cs @@ -0,0 +1,9 @@ +namespace Oqtane.Maui; + +public partial class MainPage : ContentPage +{ + public MainPage() + { + InitializeComponent(); + } +} diff --git a/Oqtane.Maui/MauiProgram.cs b/Oqtane.Maui/MauiProgram.cs new file mode 100644 index 00000000..06c9492c --- /dev/null +++ b/Oqtane.Maui/MauiProgram.cs @@ -0,0 +1,154 @@ +using System.IO.Compression; +using System.Reflection; +using System.Runtime.Loader; +using System.Diagnostics; +using Oqtane.Modules; +using Oqtane.Services; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Oqtane.Maui; + +public static class MauiProgram +{ + static string url = "http://localhost:44357"; // can be overridden in an appsettings.json in AppDataDirectory + + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + }); + + builder.Services.AddMauiBlazorWebView(); + #if DEBUG + builder.Services.AddBlazorWebViewDeveloperTools(); +#endif + + LoadAppSettings(); + + var httpClient = new HttpClient { BaseAddress = new Uri(url) }; + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(Shared.Constants.MauiUserAgent); + builder.Services.AddSingleton(httpClient); + builder.Services.AddHttpClient(); // IHttpClientFactory for calling remote services via RemoteServiceBase + + // dynamically load client assemblies + LoadClientAssemblies(httpClient); + + // register localization services + builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); + + // register auth services + builder.Services.AddOqtaneAuthorization(); + + // register scoped core services + builder.Services.AddOqtaneScopedServices(); + + var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies(); + foreach (var assembly in assemblies) + { + // dynamically register module services + RegisterModuleServices(assembly, builder.Services); + + // register client startup services + RegisterClientStartups(assembly, builder.Services); + } + + return builder.Build(); + } + + private static void LoadAppSettings() + { + string file = Path.Combine(FileSystem.Current.AppDataDirectory, "appsettings.json"); + using FileStream stream = File.OpenRead(file); + using StreamReader reader = new StreamReader(stream); + var content = reader.ReadToEnd(); + var obj = JsonSerializer.Deserialize(content)!; + if (!string.IsNullOrEmpty((string)obj["Url"])) + { + url = (string)obj["Url"]; + } + } + + private static void LoadClientAssemblies(HttpClient http) + { + try + { + // get list of loaded assemblies on the client + var assemblies = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToList(); + + // get assemblies from server and load into client app domain + var zip = http.GetByteArrayAsync($"/api/Installation/load").Result; + + // asemblies and debug symbols are packaged in a zip file + using (ZipArchive archive = new ZipArchive(new MemoryStream(zip))) + { + var dlls = new Dictionary(); + var pdbs = new Dictionary(); + + foreach (ZipArchiveEntry entry in archive.Entries) + { + if (!assemblies.Contains(Path.GetFileNameWithoutExtension(entry.FullName))) + { + using (var memoryStream = new MemoryStream()) + { + entry.Open().CopyTo(memoryStream); + byte[] file = memoryStream.ToArray(); + switch (Path.GetExtension(entry.FullName)) + { + case ".dll": + dlls.Add(entry.FullName, file); + break; + case ".pdb": + pdbs.Add(entry.FullName, file); + break; + } + } + } + } + + foreach (var item in dlls) + { + if (pdbs.ContainsKey(item.Key)) + { + AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value), new MemoryStream(pdbs[item.Key])); + } + else + { + AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value)); + } + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"Oqtane Error: Loading Client Assemblies {ex}"); + } + } + + private static void RegisterModuleServices(Assembly assembly, IServiceCollection services) + { + // dynamically register module scoped services + var implementationTypes = assembly.GetInterfaces(); + foreach (var implementationType in implementationTypes) + { + if (implementationType.AssemblyQualifiedName != null) + { + var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}")); + services.AddScoped(serviceType ?? implementationType, implementationType); + } + } + } + + private static void RegisterClientStartups(Assembly assembly, IServiceCollection services) + { + var startUps = assembly.GetInstances(); + foreach (var startup in startUps) + { + startup.ConfigureServices(services); + } + } +} diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj new file mode 100644 index 00000000..b1d0bff4 --- /dev/null +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -0,0 +1,68 @@ + + + + net6.0-android;net6.0-ios;net6.0-maccatalyst + $(TargetFrameworks);net6.0-windows10.0.19041.0 + + + Exe + Oqtane.Maui + true + true + enable + false + + + Oqtane.Maui + + + com.oqtane.maui + 0E29FC31-1B83-48ED-B6E0-9F3C67B775D4 + + + 3.1.4 + 1 + + 14.2 + 14.0 + 24.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\Oqtane.Server\bin\Debug\net6.0\Oqtane.Client.dll + + + ..\Oqtane.Server\bin\Debug\net6.0\Oqtane.Shared.dll + + + + diff --git a/Oqtane.Maui/Platforms/Android/AndroidManifest.xml b/Oqtane.Maui/Platforms/Android/AndroidManifest.xml new file mode 100644 index 00000000..e9937ad7 --- /dev/null +++ b/Oqtane.Maui/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Oqtane.Maui/Platforms/Android/MainActivity.cs b/Oqtane.Maui/Platforms/Android/MainActivity.cs new file mode 100644 index 00000000..bcb3c791 --- /dev/null +++ b/Oqtane.Maui/Platforms/Android/MainActivity.cs @@ -0,0 +1,10 @@ +using Android.App; +using Android.Content.PM; +using Android.OS; + +namespace Oqtane.Maui; + +[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +public class MainActivity : MauiAppCompatActivity +{ +} diff --git a/Oqtane.Maui/Platforms/Android/MainApplication.cs b/Oqtane.Maui/Platforms/Android/MainApplication.cs new file mode 100644 index 00000000..38c8e6ed --- /dev/null +++ b/Oqtane.Maui/Platforms/Android/MainApplication.cs @@ -0,0 +1,15 @@ +using Android.App; +using Android.Runtime; + +namespace Oqtane.Maui; + +[Application] +public class MainApplication : MauiApplication +{ + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/Oqtane.Maui/Platforms/Android/Resources/values/colors.xml b/Oqtane.Maui/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 00000000..c04d7492 --- /dev/null +++ b/Oqtane.Maui/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/Oqtane.Maui/Platforms/MacCatalyst/AppDelegate.cs b/Oqtane.Maui/Platforms/MacCatalyst/AppDelegate.cs new file mode 100644 index 00000000..ea18c92f --- /dev/null +++ b/Oqtane.Maui/Platforms/MacCatalyst/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace Oqtane.Maui; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/Oqtane.Maui/Platforms/MacCatalyst/Info.plist b/Oqtane.Maui/Platforms/MacCatalyst/Info.plist new file mode 100644 index 00000000..c96dd0a2 --- /dev/null +++ b/Oqtane.Maui/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,30 @@ + + + + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/Oqtane.Maui/Platforms/MacCatalyst/Program.cs b/Oqtane.Maui/Platforms/MacCatalyst/Program.cs new file mode 100644 index 00000000..1f902d0e --- /dev/null +++ b/Oqtane.Maui/Platforms/MacCatalyst/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace Oqtane.Maui; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} \ No newline at end of file diff --git a/Oqtane.Maui/Platforms/Tizen/Main.cs b/Oqtane.Maui/Platforms/Tizen/Main.cs new file mode 100644 index 00000000..e9860c5a --- /dev/null +++ b/Oqtane.Maui/Platforms/Tizen/Main.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Maui; +using Microsoft.Maui.Hosting; + +namespace Oqtane.Maui; + +class Program : MauiApplication +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); + + static void Main(string[] args) + { + var app = new Program(); + app.Run(args); + } +} diff --git a/Oqtane.Maui/Platforms/Tizen/tizen-manifest.xml b/Oqtane.Maui/Platforms/Tizen/tizen-manifest.xml new file mode 100644 index 00000000..dc813535 --- /dev/null +++ b/Oqtane.Maui/Platforms/Tizen/tizen-manifest.xml @@ -0,0 +1,15 @@ + + + + + + appicon.xhigh.png + + + + + http://tizen.org/privilege/internet + + + + \ No newline at end of file diff --git a/Oqtane.Maui/Platforms/Windows/App.xaml b/Oqtane.Maui/Platforms/Windows/App.xaml new file mode 100644 index 00000000..9ace8d3d --- /dev/null +++ b/Oqtane.Maui/Platforms/Windows/App.xaml @@ -0,0 +1,8 @@ + + + diff --git a/Oqtane.Maui/Platforms/Windows/App.xaml.cs b/Oqtane.Maui/Platforms/Windows/App.xaml.cs new file mode 100644 index 00000000..334e0b14 --- /dev/null +++ b/Oqtane.Maui/Platforms/Windows/App.xaml.cs @@ -0,0 +1,24 @@ +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace Oqtane.Maui.WinUI; + +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : MauiWinUIApplication +{ + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} + diff --git a/Oqtane.Maui/Platforms/Windows/Package.appxmanifest b/Oqtane.Maui/Platforms/Windows/Package.appxmanifest new file mode 100644 index 00000000..2bcb11ed --- /dev/null +++ b/Oqtane.Maui/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,43 @@ + + + + + + + $placeholder$ + User Name + $placeholder$.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Oqtane.Maui/Platforms/Windows/app.manifest b/Oqtane.Maui/Platforms/Windows/app.manifest new file mode 100644 index 00000000..669a2d9a --- /dev/null +++ b/Oqtane.Maui/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/Oqtane.Maui/Platforms/iOS/AppDelegate.cs b/Oqtane.Maui/Platforms/iOS/AppDelegate.cs new file mode 100644 index 00000000..ea18c92f --- /dev/null +++ b/Oqtane.Maui/Platforms/iOS/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace Oqtane.Maui; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/Oqtane.Maui/Platforms/iOS/Info.plist b/Oqtane.Maui/Platforms/iOS/Info.plist new file mode 100644 index 00000000..0004a4fd --- /dev/null +++ b/Oqtane.Maui/Platforms/iOS/Info.plist @@ -0,0 +1,32 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/Oqtane.Maui/Platforms/iOS/Program.cs b/Oqtane.Maui/Platforms/iOS/Program.cs new file mode 100644 index 00000000..64159aa8 --- /dev/null +++ b/Oqtane.Maui/Platforms/iOS/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace Oqtane.Maui; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/Oqtane.Maui/Properties/launchSettings.json b/Oqtane.Maui/Properties/launchSettings.json new file mode 100644 index 00000000..edf8aadc --- /dev/null +++ b/Oqtane.Maui/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "MsixPackage", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/Oqtane.Maui/Resources/AppIcon/appicon.svg b/Oqtane.Maui/Resources/AppIcon/appicon.svg new file mode 100644 index 00000000..9d63b651 --- /dev/null +++ b/Oqtane.Maui/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Oqtane.Maui/Resources/AppIcon/appiconfg.svg b/Oqtane.Maui/Resources/AppIcon/appiconfg.svg new file mode 100644 index 00000000..21dfb25f --- /dev/null +++ b/Oqtane.Maui/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Oqtane.Maui/Resources/Fonts/OpenSans-Regular.ttf b/Oqtane.Maui/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 00000000..563a5baa Binary files /dev/null and b/Oqtane.Maui/Resources/Fonts/OpenSans-Regular.ttf differ diff --git a/Oqtane.Maui/Resources/Images/dotnet_bot.svg b/Oqtane.Maui/Resources/Images/dotnet_bot.svg new file mode 100644 index 00000000..abfaff26 --- /dev/null +++ b/Oqtane.Maui/Resources/Images/dotnet_bot.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Oqtane.Maui/Resources/Splash/splash.svg b/Oqtane.Maui/Resources/Splash/splash.svg new file mode 100644 index 00000000..21dfb25f --- /dev/null +++ b/Oqtane.Maui/Resources/Splash/splash.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Oqtane.Maui/_Imports.razor b/Oqtane.Maui/_Imports.razor new file mode 100644 index 00000000..1445aeb8 --- /dev/null +++ b/Oqtane.Maui/_Imports.razor @@ -0,0 +1,7 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Oqtane.Maui diff --git a/Oqtane.Maui/wwwroot/css/app.css b/Oqtane.Maui/wwwroot/css/app.css new file mode 100644 index 00000000..5da25ceb --- /dev/null +++ b/Oqtane.Maui/wwwroot/css/app.css @@ -0,0 +1,215 @@ +@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); + +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +app { + position: relative; + display: flex; + flex-direction: column; +} + +/* Admin Modal */ +.app-admin-modal .modal { + position: fixed; /* Stay in place */ + z-index: 9999; /* Sit on top */ + left: 0; + top: 0; + display: block; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background: rgba(0,0,0,0.3); /* Dim background */ +} + + .app-admin-modal .modal-dialog { + width: 100%; /* Full width */ + height: 100%; /* Full height */ + max-width: none; /* Override default of 500px */ + } + + .app-admin-modal .modal-content { + margin: 5% auto; /* 5% from the top and centered */ + width: 80%; /* Could be more or less, depending on screen size */ + } + +/* Action Dialog */ +.app-actiondialog .modal { + position: fixed; /* Stay in place */ + z-index: 9999; /* Sit on top */ + left: 0; + top: 0; + display: block; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background: rgba(0,0,0,0.3); /* Dim background */ +} + +.app-actiondialog .modal-dialog { + width: 100%; /* Full width */ + height: 100%; /* Full height */ + max-width: none; /* Override default of 500px */ +} + +.app-actiondialog .modal-content { + margin: 15% auto; /* 15% from the top and centered */ + width: 40%; /* Could be more or less, depending on screen size */ +} + +/* Admin Pane */ +.app-pane-admin-border { + width: 100%; + border-width: 1px; + border-style: dashed; + border-color: gray; +} + +.app-pane-admin-title { + width: 100%; + text-align: center; + color: gray; +} + +.app-moduleactions .dropdown-submenu { + position: relative; +} + + .app-moduleactions .dropdown-submenu > .dropdown-menu { + top: 0; + left: 100%; + margin-top: 0px; + margin-left: 0px; + } + +.app-progress-indicator { + background: rgba(0,0,0,0.2) url('../loading.gif') no-repeat 50% 50%; + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + z-index: 9999; /* Sit on top */ +} + +.app-rule { + width: 100%; + color: gray; + height: 1px; + background-color: gray; + margin: 0.5rem; +} + +.app-link-unstyled, .app-link-unstyled:visited, .app-link-unstyled:hover, .app-link-unstyled:active, .app-link-unstyled:focus, .app-link-unstyled:active:hover { + font-style: inherit; + color: inherit; + background-color: transparent; + font-size: inherit; + text-decoration: none; + font-variant: inherit; + font-weight: inherit; + line-height: inherit; + font-family: inherit; + border-radius: inherit; + border: inherit; + outline: inherit; + box-shadow: inherit; + padding: inherit; + vertical-align: inherit; +} + +.app-alert { + padding: 20px; + background-color: #f44336; /* red */ + color: white; + margin-bottom: 15px; +} + +.app-moduletitle a { + scroll-margin-top: 7rem; +} + +/* Tooltips */ +.app-tooltip { + cursor: help; + position: relative; +} + + .app-tooltip::before, + .app-tooltip::after { + left: 25%; + opacity: 0; + position: absolute; + z-index: -100; + } + + .app-tooltip:hover::before, + .app-tooltip:focus::before, + .app-tooltip:hover::after, + .app-tooltip:focus::after { + opacity: 1; + transform: scale(1) translateY(0); + z-index: 100; + } + + .app-tooltip::before { + border-style: solid; + border-width: 1em 0.75em 0 0.75em; + border-color: #3E474F transparent transparent transparent; + bottom: 100%; + content: ""; + margin-left: -0.5em; + transition: all .65s cubic-bezier(.84,-0.18,.31,1.26), opacity .65s .5s; + transform: scale(.6) translateY(-90%); + } + + .app-tooltip:hover::before, + .app-tooltip:focus::before { + transition: all .65s cubic-bezier(.84,-0.18,.31,1.26) .2s; + } + + .app-tooltip::after { + background: #3E474F; + border-radius: .25em; + bottom: 140%; + color: #EDEFF0; + content: attr(data-tip); + margin-left: -8.75em; + padding: 1em; + transition: all .65s cubic-bezier(.84,-0.18,.31,1.26) .2s; + transform: scale(.6) translateY(50%); + width: 17.5em; + } + + .app-tooltip:hover::after, + .app-tooltip:focus::after { + transition: all .65s cubic-bezier(.84,-0.18,.31,1.26); + } + +@media (max-width: 760px) { + .app-tooltip::after { + font-size: .75em; + margin-left: -5em; + width: 10em; + } +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} diff --git a/Oqtane.Maui/wwwroot/css/empty.css b/Oqtane.Maui/wwwroot/css/empty.css new file mode 100644 index 00000000..e69de29b diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/FONT-LICENSE b/Oqtane.Maui/wwwroot/css/open-iconic/FONT-LICENSE new file mode 100644 index 00000000..a1dc03f3 --- /dev/null +++ b/Oqtane.Maui/wwwroot/css/open-iconic/FONT-LICENSE @@ -0,0 +1,86 @@ +SIL OPEN FONT LICENSE Version 1.1 + +Copyright (c) 2014 Waybury + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/ICON-LICENSE b/Oqtane.Maui/wwwroot/css/open-iconic/ICON-LICENSE new file mode 100644 index 00000000..2199f4a6 --- /dev/null +++ b/Oqtane.Maui/wwwroot/css/open-iconic/ICON-LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/README.md b/Oqtane.Maui/wwwroot/css/open-iconic/README.md new file mode 100644 index 00000000..6b810e47 --- /dev/null +++ b/Oqtane.Maui/wwwroot/css/open-iconic/README.md @@ -0,0 +1,114 @@ +[Open Iconic v1.1.1](http://useiconic.com/open) +=========== + +### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) + + + +## What's in Open Iconic? + +* 223 icons designed to be legible down to 8 pixels +* Super-light SVG files - 61.8 for the entire set +* SVG sprite—the modern replacement for icon fonts +* Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats +* Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats +* PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. + + +## Getting Started + +#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. + +### General Usage + +#### Using Open Iconic's SVGs + +We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). + +``` +icon name +``` + +#### Using Open Iconic's SVG Sprite + +Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. + +Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* + +``` + + + +``` + +Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. + +``` +.icon { + width: 16px; + height: 16px; +} +``` + +Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. + +``` +.icon-account-login { + fill: #f00; +} +``` + +To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). + +#### Using Open Iconic's Icon Font... + + +##### …with Bootstrap + +You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` + + +``` + +``` + + +``` + +``` + +##### …with Foundation + +You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` + +``` + +``` + + +``` + +``` + +##### …on its own + +You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` + +``` + +``` + +``` + +``` + + +## License + +### Icons + +All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). + +### Fonts + +All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css b/Oqtane.Maui/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css new file mode 100644 index 00000000..4664f2e8 --- /dev/null +++ b/Oqtane.Maui/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css @@ -0,0 +1 @@ +@font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} \ No newline at end of file diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.eot b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.eot new file mode 100644 index 00000000..f98177db Binary files /dev/null and b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.eot differ diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.otf b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.otf new file mode 100644 index 00000000..f6bd6846 Binary files /dev/null and b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.otf differ diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.svg b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.svg new file mode 100644 index 00000000..32b2c4e9 --- /dev/null +++ b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.svg @@ -0,0 +1,543 @@ + + + + + +Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 + By P.J. Onori +Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf new file mode 100644 index 00000000..fab60486 Binary files /dev/null and b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf differ diff --git a/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.woff b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.woff new file mode 100644 index 00000000..f9309988 Binary files /dev/null and b/Oqtane.Maui/wwwroot/css/open-iconic/font/fonts/open-iconic.woff differ diff --git a/Oqtane.Maui/wwwroot/favicon.ico b/Oqtane.Maui/wwwroot/favicon.ico new file mode 100644 index 00000000..550d600e Binary files /dev/null and b/Oqtane.Maui/wwwroot/favicon.ico differ diff --git a/Oqtane.Maui/wwwroot/images/checked.png b/Oqtane.Maui/wwwroot/images/checked.png new file mode 100644 index 00000000..a4100c70 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/checked.png differ diff --git a/Oqtane.Maui/wwwroot/images/error.png b/Oqtane.Maui/wwwroot/images/error.png new file mode 100644 index 00000000..0095d2f1 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/error.png differ diff --git a/Oqtane.Maui/wwwroot/images/help.png b/Oqtane.Maui/wwwroot/images/help.png new file mode 100644 index 00000000..f380be58 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/help.png differ diff --git a/Oqtane.Maui/wwwroot/images/logo-black.png b/Oqtane.Maui/wwwroot/images/logo-black.png new file mode 100644 index 00000000..9463cbf0 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/logo-black.png differ diff --git a/Oqtane.Maui/wwwroot/images/logo-white.png b/Oqtane.Maui/wwwroot/images/logo-white.png new file mode 100644 index 00000000..94454bf6 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/logo-white.png differ diff --git a/Oqtane.Maui/wwwroot/images/null.png b/Oqtane.Maui/wwwroot/images/null.png new file mode 100644 index 00000000..d0f50939 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/null.png differ diff --git a/Oqtane.Maui/wwwroot/images/unchecked.png b/Oqtane.Maui/wwwroot/images/unchecked.png new file mode 100644 index 00000000..566e60a8 Binary files /dev/null and b/Oqtane.Maui/wwwroot/images/unchecked.png differ diff --git a/Oqtane.Maui/wwwroot/index.html b/Oqtane.Maui/wwwroot/index.html new file mode 100644 index 00000000..a1469a6b --- /dev/null +++ b/Oqtane.Maui/wwwroot/index.html @@ -0,0 +1,34 @@ + + + + + + Oqtane Maui + + + + + + + + + + + +
+ +
Loading...
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + + + + \ No newline at end of file diff --git a/Oqtane.Maui/wwwroot/js/app.js b/Oqtane.Maui/wwwroot/js/app.js new file mode 100644 index 00000000..2c5d837e --- /dev/null +++ b/Oqtane.Maui/wwwroot/js/app.js @@ -0,0 +1,8 @@ +function subMenu(a) { + event.preventDefault(); + event.stopPropagation(); + + var li = a.parentElement, submenu = li.getElementsByTagName('ul')[0]; + submenu.style.display = submenu.style.display == "block" ? "none" : "block"; + return false; +} \ No newline at end of file diff --git a/Oqtane.Maui/wwwroot/js/interop.js b/Oqtane.Maui/wwwroot/js/interop.js new file mode 100644 index 00000000..b62c006c --- /dev/null +++ b/Oqtane.Maui/wwwroot/js/interop.js @@ -0,0 +1,394 @@ +var Oqtane = Oqtane || {}; + +Oqtane.Interop = { + setCookie: function (name, value, days) { + var d = new Date(); + d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); + var expires = "expires=" + d.toUTCString(); + document.cookie = name + "=" + value + ";" + expires + ";path=/"; + }, + getCookie: function (name) { + name = name + "="; + var decodedCookie = decodeURIComponent(document.cookie); + var ca = decodedCookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + return ""; + }, + updateTitle: function (title) { + if (document.title !== title) { + document.title = title; + } + }, + includeMeta: function (id, attribute, name, content, key) { + var meta; + if (id !== "" && key === "id") { + meta = document.getElementById(id); + } + else { + meta = document.querySelector("meta[" + attribute + "=\"" + CSS.escape(name) + "\"]"); + } + if (meta === null) { + meta = document.createElement("meta"); + meta.setAttribute(attribute, name); + if (id !== "") { + meta.id = id; + } + meta.content = content; + document.head.appendChild(meta); + } + else { + if (id !== "") { + meta.setAttribute("id", id); + } + if (meta.content !== content) { + meta.setAttribute("content", content); + } + } + }, + includeLink: function (id, rel, href, type, integrity, crossorigin, insertbefore) { + var link = document.querySelector("link[href=\"" + CSS.escape(href) + "\"]"); + if (link === null) { + link = document.createElement("link"); + if (id !== "") { + link.id = id; + } + link.rel = rel; + if (type !== "") { + link.type = type; + } + link.href = href; + if (integrity !== "") { + link.integrity = integrity; + } + if (crossorigin !== "") { + link.crossOrigin = crossorigin; + } + if (insertbefore === "") { + document.head.appendChild(link); + } + else { + var sibling = document.getElementById(insertbefore); + sibling.parentNode.insertBefore(link, sibling); + } + } + else { + if (link.id !== id) { + link.setAttribute('id', id); + } + if (link.rel !== rel) { + link.setAttribute('rel', rel); + } + if (type !== "") { + if (link.type !== type) { + link.setAttribute('type', type); + } + } else { + link.removeAttribute('type'); + } + if (link.href !== this.getAbsoluteUrl(href)) { + link.removeAttribute('integrity'); + link.removeAttribute('crossorigin'); + link.setAttribute('href', href); + } + if (integrity !== "") { + if (link.integrity !== integrity) { + link.setAttribute('integrity', integrity); + } + } else { + link.removeAttribute('integrity'); + } + if (crossorigin !== "") { + if (link.crossOrigin !== crossorigin) { + link.setAttribute('crossorigin', crossorigin); + } + } else { + link.removeAttribute('crossorigin'); + } + } + }, + includeLinks: function (links) { + for (let i = 0; i < links.length; i++) { + this.includeLink(links[i].id, links[i].rel, links[i].href, links[i].type, links[i].integrity, links[i].crossorigin, links[i].insertbefore); + } + }, + includeScript: function (id, src, integrity, crossorigin, content, location) { + var script = document.querySelector("script[src=\"" + CSS.escape(src) + "\"]"); + if (script === null) { + script = document.createElement("script"); + if (id !== "") { + script.id = id; + } + if (src !== "") { + script.src = src; + if (integrity !== "") { + script.integrity = integrity; + } + if (crossorigin !== "") { + script.crossOrigin = crossorigin; + } + } + else { + script.innerHTML = content; + } + script.async = false; + this.addScript(script, location) + .then(() => { + console.log(src + ' loaded'); + }) + .catch(() => { + console.error(src + ' failed'); + }); + } + else { + if (script.id !== id) { + script.setAttribute('id', id); + } + if (src !== "") { + if (script.src !== this.getAbsoluteUrl(src)) { + script.removeAttribute('integrity'); + script.removeAttribute('crossorigin'); + script.src = src; + } + if (integrity !== "") { + if (script.integrity !== integrity) { + script.setAttribute('integrity', integrity); + } + } else { + script.removeAttribute('integrity'); + } + if (crossorigin !== "") { + if (script.crossOrigin !== crossorigin) { + script.setAttribute('crossorigin', crossorigin); + } + } else { + script.removeAttribute('crossorigin'); + } + } + else { + if (script.innerHTML !== content) { + script.innerHTML = content; + } + } + } + }, + addScript: function (script, location) { + if (location === 'head') { + document.head.appendChild(script); + } + if (location === 'body') { + document.body.appendChild(script); + } + + return new Promise((res, rej) => { + script.onload = res(); + script.onerror = rej(); + }); + }, + includeScripts: async function (scripts) { + const bundles = []; + for (let s = 0; s < scripts.length; s++) { + if (scripts[s].bundle === '') { + scripts[s].bundle = scripts[s].href; + } + if (!bundles.includes(scripts[s].bundle)) { + bundles.push(scripts[s].bundle); + } + } + const promises = []; + for (let b = 0; b < bundles.length; b++) { + const urls = []; + for (let s = 0; s < scripts.length; s++) { + if (scripts[s].bundle === bundles[b]) { + urls.push(scripts[s].href); + } + } + promises.push(new Promise((resolve, reject) => { + if (loadjs.isDefined(bundles[b])) { + resolve(true); + } + else { + loadjs(urls, bundles[b], { + async: false, + returnPromise: true, + before: function (path, element) { + for (let s = 0; s < scripts.length; s++) { + if (path === scripts[s].href && scripts[s].integrity !== '') { + element.integrity = scripts[s].integrity; + } + if (path === scripts[s].href && scripts[s].crossorigin !== '') { + element.crossOrigin = scripts[s].crossorigin; + } + if (path === scripts[s].href && scripts[s].es6module === true) { + element.type = "module"; + } + } + } + }) + .then(function () { resolve(true) }) + .catch(function (pathsNotFound) { reject(false) }); + } + })); + } + if (promises.length !== 0) { + await Promise.all(promises); + } + }, + getAbsoluteUrl: function (url) { + var a = document.createElement('a'); + getAbsoluteUrl = function (url) { + a.href = url; + return a.href; + } + return getAbsoluteUrl(url); + }, + removeElementsById: function (prefix, first, last) { + var elements = document.querySelectorAll('[id^=' + prefix + ']'); + for (var i = elements.length - 1; i >= 0; i--) { + var element = elements[i]; + if (element.id.startsWith(prefix) && (first === '' || element.id >= first) && (last === '' || element.id <= last)) { + element.parentNode.removeChild(element); + } + } + }, + getElementByName: function (name) { + var elements = document.getElementsByName(name); + if (elements.length) { + return elements[0].value; + } else { + return ""; + } + }, + submitForm: function (path, fields) { + const form = document.createElement('form'); + form.method = 'post'; + form.action = path; + + for (const key in fields) { + if (fields.hasOwnProperty(key)) { + const hiddenField = document.createElement('input'); + hiddenField.type = 'hidden'; + hiddenField.name = key; + hiddenField.value = fields[key]; + form.appendChild(hiddenField); + } + } + + document.body.appendChild(form); + form.submit(); + }, + getFiles: function (id) { + var files = []; + var fileinput = document.getElementById(id); + if (fileinput !== null) { + for (var i = 0; i < fileinput.files.length; i++) { + files.push(fileinput.files[i].name); + } + } + return files; + }, + uploadFiles: function (posturl, folder, id, antiforgerytoken) { + var fileinput = document.getElementById(id + 'FileInput'); + var files = fileinput.files; + var progressinfo = document.getElementById(id + 'ProgressInfo'); + var progressbar = document.getElementById(id + 'ProgressBar'); + + progressinfo.setAttribute("style", "display: inline;"); + progressbar.setAttribute("style", "width: 200px; display: inline;"); + + for (var i = 0; i < files.length; i++) { + var FileChunk = []; + var file = files[i]; + var MaxFileSizeMB = 1; + var BufferChunkSize = MaxFileSizeMB * (1024 * 1024); + var FileStreamPos = 0; + var EndPos = BufferChunkSize; + var Size = file.size; + + while (FileStreamPos < Size) { + FileChunk.push(file.slice(FileStreamPos, EndPos)); + FileStreamPos = EndPos; + EndPos = FileStreamPos + BufferChunkSize; + } + + var TotalParts = FileChunk.length; + var PartCount = 0; + + while (Chunk = FileChunk.shift()) { + PartCount++; + var FileName = file.name + ".part_" + PartCount.toString().padStart(3, '0') + "_" + TotalParts.toString().padStart(3, '0'); + + var data = new FormData(); + data.append('__RequestVerificationToken', antiforgerytoken); + data.append('folder', folder); + data.append('formfile', Chunk, FileName); + var request = new XMLHttpRequest(); + request.open('POST', posturl, true); + request.upload.onloadstart = function (e) { + progressinfo.innerHTML = file.name + ' 0%'; + progressbar.value = 0; + }; + request.upload.onprogress = function (e) { + var percent = Math.ceil((e.loaded / e.total) * 100); + progressinfo.innerHTML = file.name + '[' + PartCount + '] ' + percent + '%'; + progressbar.value = (percent / 100); + }; + request.upload.onloadend = function (e) { + progressinfo.innerHTML = file.name + ' 100%'; + progressbar.value = 1; + }; + request.upload.onerror = function () { + progressinfo.innerHTML = file.name + ' Error: ' + xhr.status; + progressbar.value = 0; + }; + request.send(data); + } + + if (i === files.length - 1) { + fileinput.value = ''; + } + } + }, + refreshBrowser: function (reload, wait) { + setInterval(function () { + window.location.reload(reload); + }, wait * 1000); + }, + redirectBrowser: function (url, wait) { + setInterval(function () { + window.location.href = url; + }, wait * 1000); + }, + formValid: function (formRef) { + return formRef.checkValidity(); + }, + setElementAttribute: function (id, attribute, value) { + var element = document.getElementById(id); + if (element !== null) { + element.setAttribute(attribute, value); + } + }, + scrollTo: function (top, left, behavior) { + window.scrollTo({ + top: top, + left: left, + behavior: behavior + }); + }, + scrollToId: function (id) { + var element = document.getElementById(id); + if (element instanceof HTMLElement) { + element.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "nearest" + }); + } +}}; diff --git a/Oqtane.Maui/wwwroot/js/loadjs.min.js b/Oqtane.Maui/wwwroot/js/loadjs.min.js new file mode 100644 index 00000000..b2165fc3 --- /dev/null +++ b/Oqtane.Maui/wwwroot/js/loadjs.min.js @@ -0,0 +1 @@ +loadjs=function(){var h=function(){},c={},u={},f={};function o(e,n){if(e){var r=f[e];if(u[e]=n,r)for(;r.length;)r[0](e,n),r.splice(0,1)}}function l(e,n){e.call&&(e={success:e}),n.length?(e.error||h)(n):(e.success||h)(e)}function d(r,t,s,i){var c,o,e=document,n=s.async,u=(s.numRetries||0)+1,f=s.before||h,l=r.replace(/[\?|#].*$/,""),a=r.replace(/^(css|img)!/,"");i=i||0,/(^css!|\.css$)/.test(l)?((o=e.createElement("link")).rel="stylesheet",o.href=a,(c="hideFocus"in o)&&o.relList&&(c=0,o.rel="preload",o.as="style")):/(^img!|\.(png|gif|jpg|svg|webp)$)/.test(l)?(o=e.createElement("img")).src=a:((o=e.createElement("script")).src=r,o.async=void 0===n||n),!(o.onload=o.onerror=o.onbeforeload=function(e){var n=e.type[0];if(c)try{o.sheet.cssText.length||(n="e")}catch(e){18!=e.code&&(n="e")}if("e"==n){if((i+=1)/login [HttpPost("login")] - public async Task Login([FromBody] User user) + public async Task Login([FromBody] User user, bool setCookie, bool isPersistent) { User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false }; @@ -358,6 +358,11 @@ namespace Oqtane.Controllers loginUser.LastIPAddress = LastIPAddress; _users.UpdateUser(loginUser); _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username); + + if (setCookie) + { + await _identitySignInManager.SignInAsync(identityuser, isPersistent); + } } else { diff --git a/Oqtane.Server/Infrastructure/TenantManager.cs b/Oqtane.Server/Infrastructure/TenantManager.cs index de3ca94e..8ebcc20d 100644 --- a/Oqtane.Server/Infrastructure/TenantManager.cs +++ b/Oqtane.Server/Infrastructure/TenantManager.cs @@ -32,11 +32,12 @@ namespace Oqtane.Infrastructure else { // if there is http context - if (_httpContextAccessor.HttpContext != null) + var httpcontext = _httpContextAccessor.HttpContext; + if (httpcontext != null) { // legacy support for client api requests which would include the alias as a path prefix ( ie. {alias}/api/[controller] ) int aliasId; - string[] segments = _httpContextAccessor.HttpContext.Request.Path.Value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + string[] segments = httpcontext.Request.Path.Value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); if (segments.Length > 1 && (segments[1] == "api" || segments[1] == "pages") && int.TryParse(segments[0], out aliasId)) { alias = _aliasRepository.GetAliases().ToList().FirstOrDefault(item => item.AliasId == aliasId); @@ -45,13 +46,19 @@ namespace Oqtane.Infrastructure // resolve alias based on host name and path if (alias == null) { - string name = _httpContextAccessor.HttpContext.Request.Host.Value + _httpContextAccessor.HttpContext.Request.Path; + string name = httpcontext.Request.Host.Value + httpcontext.Request.Path; alias = _aliasRepository.GetAlias(name); } // if there is a match save it if (alias != null) { + alias.Protocol = (httpcontext.Request.IsHttps) ? "https://" : "http://"; + alias.BaseUrl = ""; + if (httpcontext.Request.Headers.ContainsKey("User-Agent") && httpcontext.Request.Headers["User-Agent"] == Shared.Constants.MauiUserAgent) + { + alias.BaseUrl = alias.Protocol + alias.Name; + } _siteState.Alias = alias; } } diff --git a/Oqtane.Server/Pages/_Host.cshtml b/Oqtane.Server/Pages/_Host.cshtml index 394dd296..17ffe457 100644 --- a/Oqtane.Server/Pages/_Host.cshtml +++ b/Oqtane.Server/Pages/_Host.cshtml @@ -27,7 +27,7 @@ { @(Html.AntiForgeryToken()) - +
diff --git a/Oqtane.Server/Pages/_Host.cshtml.cs b/Oqtane.Server/Pages/_Host.cshtml.cs index 25f57884..76825fbc 100644 --- a/Oqtane.Server/Pages/_Host.cshtml.cs +++ b/Oqtane.Server/Pages/_Host.cshtml.cs @@ -8,7 +8,6 @@ using System.Reflection; using Oqtane.Repository; using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.Configuration; -using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; @@ -61,7 +60,7 @@ namespace Oqtane.Pages public string AntiForgeryToken = ""; public string AuthorizationToken = ""; public string Runtime = "Server"; - public RenderMode RenderMode = RenderMode.Server; + public string RenderMode = "ServerPrerendered"; public int VisitorId = -1; public string RemoteIPAddress = ""; public string HeadResources = ""; @@ -84,7 +83,7 @@ namespace Oqtane.Pages if (_configuration.GetSection("RenderMode").Exists()) { - RenderMode = (RenderMode)Enum.Parse(typeof(RenderMode), _configuration.GetSection("RenderMode").Value, true); + RenderMode = _configuration.GetSection("RenderMode").Value; } // if framework is installed @@ -123,7 +122,7 @@ namespace Oqtane.Pages } if (!string.IsNullOrEmpty(site.RenderMode)) { - RenderMode = (RenderMode)Enum.Parse(typeof(RenderMode), site.RenderMode, true); + RenderMode = site.RenderMode; } if (site.FaviconFileId != null) { @@ -242,7 +241,8 @@ namespace Oqtane.Pages try { // get request attributes - string useragent = (Request.Headers[HeaderNames.UserAgent] != StringValues.Empty) ? Request.Headers[HeaderNames.UserAgent].ToString().Substring(0,256) : "(none)"; + string useragent = (Request.Headers[HeaderNames.UserAgent] != StringValues.Empty) ? Request.Headers[HeaderNames.UserAgent] : "(none)"; + useragent = (useragent.Length > 256) ? useragent.Substring(0, 256) : useragent; string language = (Request.Headers[HeaderNames.AcceptLanguage] != StringValues.Empty) ? Request.Headers[HeaderNames.AcceptLanguage] : ""; language = (language.Contains(",")) ? language.Substring(0, language.IndexOf(",")) : language; language = (language.Contains(";")) ? language.Substring(0, language.IndexOf(";")) : language; diff --git a/Oqtane.Server/Repository/FileRepository.cs b/Oqtane.Server/Repository/FileRepository.cs index 4ac5403c..dba0d491 100644 --- a/Oqtane.Server/Repository/FileRepository.cs +++ b/Oqtane.Server/Repository/FileRepository.cs @@ -112,7 +112,7 @@ namespace Oqtane.Repository url = Utilities.ContentUrl(alias, file.FileId); break; case FolderTypes.Public: - url = "/" + Utilities.UrlCombine("Content", "Tenants", alias.TenantId.ToString(), "Sites", file.Folder.SiteId.ToString(), file.Folder.Path) + file.Name; + url = alias.BaseUrl + Utilities.UrlCombine("Content", "Tenants", alias.TenantId.ToString(), "Sites", file.Folder.SiteId.ToString(), file.Folder.Path) + file.Name; break; } return url; diff --git a/Oqtane.Server/Security/AutoValidateAntiforgeryTokenFilter.cs b/Oqtane.Server/Security/AutoValidateAntiforgeryTokenFilter.cs index 5efe69fc..8652ce89 100644 --- a/Oqtane.Server/Security/AutoValidateAntiforgeryTokenFilter.cs +++ b/Oqtane.Server/Security/AutoValidateAntiforgeryTokenFilter.cs @@ -51,7 +51,7 @@ namespace Oqtane.Security protected virtual bool ShouldValidate(AuthorizationFilterContext context) { // ignore antiforgery validation if a bearer token was provided - if (context.HttpContext.Request.Headers.ContainsKey("Authorization")) + if (context.HttpContext.Request.Headers.ContainsKey("Authorization") || context.HttpContext.Request.Headers["User-Agent"] == Constants.MauiUserAgent) { return false; } diff --git a/Oqtane.Shared/Enums/Runtime.cs b/Oqtane.Shared/Enums/Runtime.cs index 4cca8235..3758b765 100644 --- a/Oqtane.Shared/Enums/Runtime.cs +++ b/Oqtane.Shared/Enums/Runtime.cs @@ -3,6 +3,7 @@ namespace Oqtane.Shared public enum Runtime { Server, - WebAssembly + WebAssembly, + Hybrid } } diff --git a/Oqtane.Shared/Models/Alias.cs b/Oqtane.Shared/Models/Alias.cs index 70e1c886..c3697be0 100644 --- a/Oqtane.Shared/Models/Alias.cs +++ b/Oqtane.Shared/Models/Alias.cs @@ -82,9 +82,15 @@ namespace Oqtane.Models } /// - /// Site-specific settings (only available on the server via HttpContext for security reasons) + /// Protocol for the request from which the alias was resolved (ie. http or https ) /// - //[NotMapped] - //public Dictionary SiteSettings { get; set; } + [NotMapped] + public string Protocol { get; set; } + + /// + /// Base Url for static resources (note that this will only be set for remote clients) + /// + [NotMapped] + public string BaseUrl { get; set; } } } diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 627a57e4..350b6f52 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -89,5 +89,8 @@ namespace Oqtane.Shared public static readonly string HttpContextAliasKey = "Alias"; public static readonly string HttpContextSiteSettingsKey = "SiteSettings"; + + public static readonly string MauiUserAgent = "MAUI"; + } } diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index 89b33341..db3b439c 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -108,7 +108,7 @@ namespace Oqtane.Shared var aliasUrl = (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path : ""; var method = asAttachment ? "/attach" : ""; - return $"{aliasUrl}{Constants.ContentUrl}{fileId}{method}"; + return $"{alias.BaseUrl}{aliasUrl}{Constants.ContentUrl}{fileId}{method}"; } public static string ImageUrl(Alias alias, int fileId, int width, int height, string mode) @@ -118,17 +118,18 @@ namespace Oqtane.Shared public static string ImageUrl(Alias alias, int fileId, int width, int height, string mode, string position, string background, int rotate, bool recreate) { - var aliasUrl = (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path : ""; + var url = (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path : ""; mode = string.IsNullOrEmpty(mode) ? "crop" : mode; position = string.IsNullOrEmpty(position) ? "center" : position; background = string.IsNullOrEmpty(background) ? "000000" : background; - return $"{aliasUrl}{Constants.ImageUrl}{fileId}/{width}/{height}/{mode}/{position}/{background}/{rotate}/{recreate}"; + return $"{alias.BaseUrl}{url}{Constants.ImageUrl}{fileId}/{width}/{height}/{mode}/{position}/{background}/{rotate}/{recreate}"; } public static string TenantUrl(Alias alias, string url) { url = (!url.StartsWith("/")) ? "/" + url : url; - return (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path + url : url; + url = (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path + url : url; + return $"{alias.BaseUrl}{url}"; } public static string FormatContent(string content, Alias alias, string operation) diff --git a/Oqtane.sln b/Oqtane.sln index f82fa77c..2ef03009 100644 --- a/Oqtane.sln +++ b/Oqtane.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28822.285 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32611.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Server", "Oqtane.Server\Oqtane.Server.csproj", "{083BB22D-DF24-43A2-95E5-8F385CCB3318}" EndProject