Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ThemeProvider): support auto mode #4400

Merged
merged 12 commits into from
Oct 7, 2024
1 change: 0 additions & 1 deletion src/BootstrapBlazor.Server/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
<Link Href="_content/BootstrapBlazor/css/motronic.min.css" />
<Link Href="BootstrapBlazor.Server.styles.css" />
<Link Href="css/site.css" />
<ThemeLoader></ThemeLoader>
<HeadOutlet @rendermode="new InteractiveServerRenderMode(false)" />
<title>@Localizer["Title"]</title>
</head>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,5 @@ protected override void OnInitialized()
_versionString = $"v{PackageVersionService.Version}";
}

private Task OnThemeChangedAsync(string themeName) => InvokeVoidAsync("updateTheme", themeName);
private Task OnThemeChangedAsync(ThemeValue themeName) => InvokeVoidAsync("updateTheme", themeName);
}
18 changes: 3 additions & 15 deletions src/BootstrapBlazor.Server/Controllers/Api/LoginController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,18 @@
namespace BootstrapBlazor.Server.Controllers.Api;

/// <summary>
///
/// 登录控制器
/// </summary>
[Route("api/[controller]")]
[AllowAnonymous]
[ApiController]
public class LoginController : ControllerBase
{
/// <summary>
///
/// 认证方法
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
[HttpPost]
public IActionResult Post(User user)
{
IActionResult? response;
if (user.UserName == "admin" && user.Password == "123456")
{
response = new JsonResult(new { Code = 200, Message = "登录成功" });
}
else
{
response = new JsonResult(new { Code = 500, Message = "用户名或密码错误" });
}
return response;
}
public IActionResult Post(User user) => user is { UserName: "admin", Password: "123456" } ? new JsonResult(new { Code = 200, Message = "登录成功" }) : new JsonResult(new { Code = 500, Message = "用户名或密码错误" });
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,19 @@ public partial class ThemeProvider
/// 获得/设置 主题切换回调方法
/// </summary>
[Parameter]
public Func<string, Task>? OnThemeChangedAsync { get; set; }
public Func<ThemeValue, Task>? OnThemeChangedAsync { get; set; }

/// <summary>
/// 主题类型
/// </summary>
[Parameter]
public ThemeValue ThemeValue { get; set; } = ThemeValue.UseLocalStorage;

/// <summary>
/// 主题类型改变回调方法
/// </summary>
[Parameter]
public EventCallback<ThemeValue> ThemeValueChanged { get; set; }

[Inject, NotNull]
private IIconTheme? IconTheme { get; set; }
Expand Down Expand Up @@ -107,16 +119,21 @@ protected override void OnParametersSet()
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, OnThemeChangedAsync != null ? nameof(OnThemeChanged) : null);
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, ThemeValue, nameof(OnThemeChanged));

/// <summary>
/// JavaScript 回调方法
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
[JSInvokable]
public async Task OnThemeChanged(string name)
public async Task OnThemeChanged(ThemeValue name)
{
if (ThemeValueChanged.HasDelegate)
{
await ThemeValueChanged.InvokeAsync(name);
}

if (OnThemeChangedAsync != null)
{
await OnThemeChangedAsync(name);
Expand Down
75 changes: 47 additions & 28 deletions src/BootstrapBlazor/Components/ThemeProvider/ThemeProvider.razor.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,56 @@
import { getAutoThemeValue, getPreferredTheme, setActiveTheme, switchTheme } from "../../modules/utility.js"
import { getPreferredTheme, setTheme, switchTheme } from "../../modules/utility.js"
import EventHandler from "../../modules/event-handler.js"
import Data from "../../modules/data.js"

export function init(id, invoke, callback) {
export function init(id, invoke, themeValue, callback) {
const el = document.getElementById(id);
if (el) {
const currentTheme = getPreferredTheme();
const activeItem = el.querySelector(`.dropdown-item[data-bb-theme-value="${currentTheme}"]`);
if (activeItem) {
setActiveTheme(el, activeItem);
}
if (el === null) {
return;
}

const theme = { el };
Data.set(id, theme);

const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
EventHandler.on(darkModeMediaQuery, 'change', () => changeTheme(id));
theme.mediaQueryList = darkModeMediaQuery;

const items = el.querySelectorAll('.dropdown-item');
items.forEach(item => {
EventHandler.on(item, 'click', () => {
setActiveTheme(el, item);

let theme = item.getAttribute('data-bb-theme-value');
if (theme === 'auto') {
theme = getAutoThemeValue();
}
switchTheme(theme, window.innerWidth, 0);
if (callback) {
invoke.invokeMethodAsync(callback, theme);
}
});
});
let currentTheme = themeValue;
if (currentTheme === 'useLocalStorage') {
currentTheme = getPreferredTheme();
}
setTheme(currentTheme, true);
theme.currentTheme = currentTheme;

EventHandler.on(el, 'click', '.dropdown-item', e => {
const activeTheme = e.delegateTarget.getAttribute('data-bb-theme-value');
theme.currentTheme = activeTheme;
switchTheme(activeTheme, window.innerWidth, 0);
if (callback) {
invoke.invokeMethodAsync(callback, activeTheme);
}
});
}

export function dispose(id) {
const el = document.getElementById(id);
const items = el.querySelectorAll('.dropdown-item');
items.forEach(item => {
EventHandler.off(item, 'click');
});
const theme = Data.get(id);
if (theme === null) {
return;
}
Data.remove(id);

const { el, darkModeMediaQuery } = theme;
EventHandler.off(el, 'click');
EventHandler.off(darkModeMediaQuery, 'change');
}

const changeTheme = id => {
const theme = Data.get(id);
if (theme === null) {
return;
}

if (theme.currentTheme === 'auto') {
switchTheme('auto', window.innerWidth, 0);
}
}
34 changes: 34 additions & 0 deletions src/BootstrapBlazor/Components/ThemeProvider/ThemeValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/

using BootstrapBlazor.Core.Converter;

namespace BootstrapBlazor.Components;

/// <summary>
/// 主题选项
/// </summary>
[JsonEnumConverter(true)]
public enum ThemeValue
{
/// <summary>
/// 自动
/// </summary>
Auto,

/// <summary>
/// 明亮主题
/// </summary>
Light,

/// <summary>
/// 暗黑主题
/// </summary>
Dark,

/// <summary>
/// 使用本地保存选项
/// </summary>
UseLocalStorage,
}
24 changes: 8 additions & 16 deletions src/BootstrapBlazor/Converter/JsonEnumConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,26 @@ namespace BootstrapBlazor.Core.Converter;
/// </summary>
public class JsonEnumConverter : JsonConverterAttribute
{
/// <summary>
/// 构造函数
/// </summary>
public JsonEnumConverter() : base()
{

}

/// <summary>
/// 构造函数
/// </summary>
/// <param name="camelCase">Optional naming policy for writing enum values.</param>
/// <param name="allowIntegerValues">True to allow undefined enum values. When true, if an enum value isn't defined it will output as a number rather than a string.</param>
public JsonEnumConverter(bool camelCase, bool allowIntegerValues = true) : this()
public JsonEnumConverter(bool camelCase = false, bool allowIntegerValues = true)
{
CamelCase = camelCase;
AllowIntegerValues = allowIntegerValues;
_camelCase = camelCase;
_allowIntegerValues = allowIntegerValues;
}

/// <summary>
/// naming policy for writing enum values
/// </summary>
public bool CamelCase { get; private set; }
private readonly bool _camelCase;

/// <summary>
/// True to allow undefined enum values. When true, if an enum value isn't defined it will output as a number rather than a string
/// </summary>
public bool AllowIntegerValues { get; private set; } = true;
private readonly bool _allowIntegerValues;

/// <summary>
/// <inheritdoc/>
Expand All @@ -48,9 +40,9 @@ public JsonEnumConverter(bool camelCase, bool allowIntegerValues = true) : this(
/// <returns></returns>
public override JsonConverter? CreateConverter(Type typeToConvert)
{
var converter = CamelCase
? new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, AllowIntegerValues)
: new JsonStringEnumConverter(null, AllowIntegerValues);
var converter = _camelCase
? new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, _allowIntegerValues)
: new JsonStringEnumConverter(null, _allowIntegerValues);
return converter;
}
}
4 changes: 3 additions & 1 deletion src/BootstrapBlazor/wwwroot/modules/utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,9 @@ export function getTheme() {
}

export function saveTheme(theme) {
localStorage.setItem('theme', theme)
if (localStorage) {
localStorage.setItem('theme', theme);
}
}

export function getAutoThemeValue() {
Expand Down
24 changes: 18 additions & 6 deletions test/UnitTest/Components/ThemeProviderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,29 @@ public void ThemeProvider_Ok()
[Fact]
public async Task OnThemeChanged_Ok()
{
var name = "";
var v = ThemeValue.Auto;
var cut = Context.RenderComponent<ThemeProvider>(pb =>
{
pb.Add(a => a.OnThemeChangedAsync, t =>
pb.Add(a => a.OnThemeChangedAsync, val =>
{
name = t;
v = val;
return Task.CompletedTask;
});
pb.Add(a => a.Alignment, Alignment.Center);
});
await cut.Instance.OnThemeChanged("dark");
Assert.Equal("dark", name);
await cut.Instance.OnThemeChanged(ThemeValue.Dark);
Assert.Equal(ThemeValue.Dark, v);
}

[Fact]
public async Task ThemeValueChanged_Ok()
{
var v = ThemeValue.Auto;
var cut = Context.RenderComponent<ThemeProvider>(pb =>
{
pb.Add(a => a.ThemeValue, ThemeValue.Light);
pb.Add(a => a.ThemeValueChanged, EventCallback.Factory.Create<ThemeValue>(this, val => v = val));
});
await cut.Instance.OnThemeChanged(ThemeValue.Dark);
Assert.Equal(ThemeValue.Dark, v);
}
}