调整
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tauri + Vue + Typescript App</title>
|
<title>Thing</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -44,13 +44,14 @@ use monitor_kernel::{
|
|||||||
};
|
};
|
||||||
use music::{
|
use music::{
|
||||||
feiniu_activate_connection, feiniu_cache_clear, feiniu_cache_fetch, feiniu_cache_status,
|
feiniu_activate_connection, feiniu_cache_clear, feiniu_cache_fetch, feiniu_cache_status,
|
||||||
feiniu_delete_connection, feiniu_delete_local, feiniu_fnconnect_resolve, feiniu_get_config,
|
feiniu_cover_data, feiniu_delete_connection, feiniu_delete_local, feiniu_fnconnect_resolve,
|
||||||
feiniu_list_connections, feiniu_list_tracks, feiniu_login, feiniu_logout, feiniu_lyric,
|
feiniu_get_config, feiniu_list_connections, feiniu_list_tracks, feiniu_login, feiniu_logout,
|
||||||
feiniu_media_prefix, feiniu_save_connection, feiniu_scan_local, feiniu_test_connection,
|
feiniu_lyric, feiniu_media_prefix, feiniu_save_connection, feiniu_scan_local,
|
||||||
music_cancel_runtime_install, music_download, music_download_cancel, music_env_status,
|
feiniu_test_connection, music_cancel_runtime_install, music_download, music_download_cancel,
|
||||||
music_get_settings, music_get_sources, music_install_runtime, music_parse_playlist, music_ping,
|
music_env_status, music_get_settings, music_get_sources, music_install_runtime,
|
||||||
music_resolve, music_save_settings, music_search, music_stop_bridge, webdav_delete,
|
music_parse_playlist, music_ping, music_resolve, music_save_settings, music_search,
|
||||||
webdav_get_secret, webdav_save_secret, webdav_test, webdav_upload, MusicManager,
|
music_stop_bridge, webdav_delete, webdav_get_secret, webdav_save_secret, webdav_test,
|
||||||
|
webdav_upload, MusicManager,
|
||||||
};
|
};
|
||||||
use network_monitor::network_status;
|
use network_monitor::network_status;
|
||||||
use osd_window::{
|
use osd_window::{
|
||||||
@@ -281,6 +282,7 @@ pub fn run() {
|
|||||||
feiniu_get_config,
|
feiniu_get_config,
|
||||||
feiniu_list_tracks,
|
feiniu_list_tracks,
|
||||||
feiniu_lyric,
|
feiniu_lyric,
|
||||||
|
feiniu_cover_data,
|
||||||
feiniu_media_prefix,
|
feiniu_media_prefix,
|
||||||
feiniu_scan_local,
|
feiniu_scan_local,
|
||||||
feiniu_cache_status,
|
feiniu_cache_status,
|
||||||
|
|||||||
@@ -380,6 +380,28 @@ pub async fn feiniu_lyric(
|
|||||||
Ok(json!({ "lyric": text }))
|
Ok(json!({ "lyric": text }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 封面 data URL(Windows SMTC / MediaSession artwork 用):
|
||||||
|
/// Rust 侧取 NAS 封面字节转 base64,规避 WebView 对 artwork 的 CORS 要求。
|
||||||
|
/// 返回 Value 且未标注 specta:前端直接按 JSON 使用(同 music_ping 豁免模式)。
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn feiniu_cover_data(
|
||||||
|
state: State<'_, MusicManager>,
|
||||||
|
cover_id: String,
|
||||||
|
size: Option<u32>,
|
||||||
|
) -> Result<serde_json::Value, String> {
|
||||||
|
state.feiniu.sync_with_settings(&state.load_settings());
|
||||||
|
let (bytes, ct) = state
|
||||||
|
.feiniu
|
||||||
|
.cover_bytes(&cover_id, size.unwrap_or(320))
|
||||||
|
.await?;
|
||||||
|
use base64::Engine;
|
||||||
|
let data = format!(
|
||||||
|
"data:{ct};base64,{}",
|
||||||
|
base64::engine::general_purpose::STANDARD.encode(bytes)
|
||||||
|
);
|
||||||
|
Ok(json!({ "data": data }))
|
||||||
|
}
|
||||||
|
|
||||||
/// 本地媒体地址前缀:{ mediaPrefix }(首次调用惰性启动本地流代理)。
|
/// 本地媒体地址前缀:{ mediaPrefix }(首次调用惰性启动本地流代理)。
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn feiniu_media_prefix(
|
pub async fn feiniu_media_prefix(
|
||||||
|
|||||||
@@ -315,6 +315,44 @@ impl Feiniu {
|
|||||||
Ok(extract_lyric_text(&v))
|
Ok(extract_lyric_text(&v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 封面二进制(MediaSession artwork 用):`GET /music/api/v1/static/cover`。
|
||||||
|
/// 返回 (字节, content-type)。与流代理同款认证注入,但不走代理——
|
||||||
|
/// 前端拿到的是 data URL,规避 WebView 对 artwork 的 CORS 要求。
|
||||||
|
pub async fn cover_bytes(&self, cover_id: &str, size: u32) -> Result<(Vec<u8>, String), String> {
|
||||||
|
let (base, token, access_code) = self.auth_triple();
|
||||||
|
if base.is_empty() || token.is_empty() {
|
||||||
|
return Err("未登录".into());
|
||||||
|
}
|
||||||
|
let (_, client) = self.conn_client();
|
||||||
|
let query = [
|
||||||
|
("coverId".to_string(), cover_id.to_string()),
|
||||||
|
("size".to_string(), size.to_string()),
|
||||||
|
];
|
||||||
|
let mut rb = client.get(format!("{base}/music/api/v1/static/cover")).query(&query);
|
||||||
|
rb = rb.header("cookie", format!("music-token={token}"));
|
||||||
|
if !access_code.is_empty() {
|
||||||
|
use base64::Engine;
|
||||||
|
rb = rb
|
||||||
|
.header(
|
||||||
|
"x-access-code",
|
||||||
|
base64::engine::general_purpose::STANDARD.encode(access_code.as_bytes()),
|
||||||
|
)
|
||||||
|
.header("x-access-source", "app");
|
||||||
|
}
|
||||||
|
let resp = rb.send().await.map_err(|e| format!("请求失败: {e}"))?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("请求失败(HTTP {})", resp.status().as_u16()));
|
||||||
|
}
|
||||||
|
let ct = resp
|
||||||
|
.headers()
|
||||||
|
.get(reqwest::header::CONTENT_TYPE)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("image/jpeg")
|
||||||
|
.to_string();
|
||||||
|
let bytes = resp.bytes().await.map_err(|e| format!("读取失败: {e}"))?;
|
||||||
|
Ok((bytes.to_vec(), ct))
|
||||||
|
}
|
||||||
|
|
||||||
/// 本地媒体地址前缀:`http://127.0.0.1:<port>/feiniu`。首次调用惰性启动代理。
|
/// 本地媒体地址前缀:`http://127.0.0.1:<port>/feiniu`。首次调用惰性启动代理。
|
||||||
pub async fn media_prefix(&self) -> Result<String, String> {
|
pub async fn media_prefix(&self) -> Result<String, String> {
|
||||||
let port = self.ensure_proxy().await?;
|
let port = self.ensure_proxy().await?;
|
||||||
|
|||||||
@@ -24,13 +24,14 @@ pub use feiniu::{extract_fn_id, normalize_base_url, resolve_base_url, Feiniu, Fe
|
|||||||
|
|
||||||
pub use commands::{
|
pub use commands::{
|
||||||
feiniu_activate_connection, feiniu_cache_clear, feiniu_cache_fetch, feiniu_cache_status,
|
feiniu_activate_connection, feiniu_cache_clear, feiniu_cache_fetch, feiniu_cache_status,
|
||||||
feiniu_delete_connection, feiniu_delete_local, feiniu_fnconnect_resolve, feiniu_get_config,
|
feiniu_cover_data, feiniu_delete_connection, feiniu_delete_local, feiniu_fnconnect_resolve,
|
||||||
feiniu_list_connections, feiniu_list_tracks, feiniu_login, feiniu_logout, feiniu_lyric,
|
feiniu_get_config, feiniu_list_connections, feiniu_list_tracks, feiniu_login, feiniu_logout,
|
||||||
feiniu_media_prefix, feiniu_save_connection, feiniu_scan_local, feiniu_test_connection,
|
feiniu_lyric, feiniu_media_prefix, feiniu_save_connection, feiniu_scan_local,
|
||||||
music_cancel_runtime_install, music_download, music_download_cancel, music_env_status,
|
feiniu_test_connection, music_cancel_runtime_install, music_download, music_download_cancel,
|
||||||
music_get_settings, music_get_sources, music_install_runtime, music_parse_playlist, music_ping,
|
music_env_status, music_get_settings, music_get_sources, music_install_runtime,
|
||||||
music_resolve, music_save_settings, music_search, music_stop_bridge, webdav_delete,
|
music_parse_playlist, music_ping, music_resolve, music_save_settings, music_search,
|
||||||
webdav_get_secret, webdav_save_secret, webdav_test, webdav_upload,
|
music_stop_bridge, webdav_delete, webdav_get_secret, webdav_save_secret, webdav_test,
|
||||||
|
webdav_upload,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset: http://asset.localhost; font-src 'self' data:; connect-src ipc: http://ipc.localhost; media-src 'self' data: blob:"
|
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset: http://asset.localhost http: https:; font-src 'self' data:; connect-src ipc: http://ipc.localhost http: https:; media-src 'self' data: blob: file: http: https:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|||||||
+122
-6
@@ -96,14 +96,14 @@ function writeLS(key: string, value: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 把浏览器抛出的播放异常翻译成可读文案 */
|
/** 把浏览器抛出的播放异常翻译成可读文案 */
|
||||||
function describePlayError(e: unknown): string {
|
function describePlayError(e: unknown): string {
|
||||||
const name = (e as { name?: string })?.name ?? ''
|
const name = (e as { name?: string })?.name ?? ''
|
||||||
if (name === 'NotSupportedError') return '该音频无法播放:格式不支持或链接已失效'
|
if (name === 'NotSupportedError') return '该音频无法播放:格式不支持或链接已失效'
|
||||||
if (name === 'NotAllowedError') return '播放被浏览器策略阻止,请再次点击播放'
|
if (name === 'NotAllowedError') return '播放被浏览器策略阻止,请再次点击播放'
|
||||||
if (name === 'AbortError') return '播放已中断'
|
if (name === 'AbortError') return '播放已中断'
|
||||||
return `播放失败:${e instanceof Error ? e.message : String(e)}`
|
return `播放失败:${e instanceof Error ? e.message : String(e)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFeiniuStore = defineStore('feiniu', () => {
|
export const useFeiniuStore = defineStore('feiniu', () => {
|
||||||
// ===== 连接 =====
|
// ===== 连接 =====
|
||||||
@@ -185,6 +185,100 @@ export const useFeiniuStore = defineStore('feiniu', () => {
|
|||||||
const shufflePool = ref<number[]>([])
|
const shufflePool = ref<number[]>([])
|
||||||
const historyStack = ref<number[]>([])
|
const historyStack = ref<number[]>([])
|
||||||
|
|
||||||
|
// ===== Windows 系统媒体(SMTC)集成 =====
|
||||||
|
// WebView2 会把页面媒体暴露给系统媒体浮层/媒体键,元数据需通过 MediaSession API 提供;
|
||||||
|
// 不提供时系统显示的是 document.title(此前为 "Tauri+Vue+Typescript App")。
|
||||||
|
|
||||||
|
/** 封面 data URL 缓存(coverId → dataUrl),避免同一首歌重复取封面 */
|
||||||
|
const smtcCoverCache = new Map<string, string>()
|
||||||
|
/** SMTC 进度上报节流(timeupdate ~4Hz,1s 一次足够) */
|
||||||
|
let smtcLastPosAt = 0
|
||||||
|
|
||||||
|
/** 设置系统媒体浮层的歌曲信息(歌名/歌手/专辑/封面)。封面为飞牛曲目时经
|
||||||
|
* Rust 命令取字节转 data URL——MediaSession artwork 由 WebView 以 CORS 模式
|
||||||
|
* 拉取,代理地址无 CORS 头会失败,data URL 可绕过。 */
|
||||||
|
function updateMediaSession(item: PlayableItem) {
|
||||||
|
if (!('mediaSession' in navigator)) return
|
||||||
|
const base = {
|
||||||
|
title: item.title,
|
||||||
|
artist: item.artistNames || '未知歌手',
|
||||||
|
album: item.album || ''
|
||||||
|
}
|
||||||
|
if (item.source === 'feiniu' && item.coverId) {
|
||||||
|
const key = item.coverId
|
||||||
|
const cached = smtcCoverCache.get(key)
|
||||||
|
// 歌名/歌手立即上屏,封面就绪后补挂
|
||||||
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
|
...base,
|
||||||
|
artwork: cached ? [{ src: cached, sizes: '320x320' }] : []
|
||||||
|
})
|
||||||
|
if (!cached) {
|
||||||
|
invoke<{ data: string }>('feiniu_cover_data', { coverId: key, size: 320 })
|
||||||
|
.then((r) => {
|
||||||
|
if (!r?.data) return
|
||||||
|
if (smtcCoverCache.size > 50) smtcCoverCache.clear()
|
||||||
|
smtcCoverCache.set(key, r.data)
|
||||||
|
// 播放期间用户可能已切歌:仅当仍是这首时才刷新封面
|
||||||
|
const cur = navigator.mediaSession.metadata
|
||||||
|
if (cur?.title === item.title) {
|
||||||
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
|
...base,
|
||||||
|
artwork: [{ src: r.data, sizes: '320x320' }]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* 封面取不到就只显示文字信息 */
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// preview 源有直链封面;local/无封面源仅文字信息
|
||||||
|
const artwork: MediaImage[] = []
|
||||||
|
if (item.coverUrl) artwork.push({ src: item.coverUrl, sizes: '320x320' })
|
||||||
|
navigator.mediaSession.metadata = new MediaMetadata({ ...base, artwork })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册系统媒体键/浮层按钮动作(一次性) */
|
||||||
|
function setupMediaSessionHandlers(a: HTMLAudioElement) {
|
||||||
|
if (!('mediaSession' in navigator)) return
|
||||||
|
const ms = navigator.mediaSession
|
||||||
|
try {
|
||||||
|
ms.setActionHandler('play', () => void a.play().catch(() => {}))
|
||||||
|
ms.setActionHandler('pause', () => a.pause())
|
||||||
|
ms.setActionHandler('previoustrack', () => prev())
|
||||||
|
ms.setActionHandler('nexttrack', () => next())
|
||||||
|
ms.setActionHandler('seekbackward', (d) => seek(Math.max(0, position.value - (d?.seekOffset ?? 10))))
|
||||||
|
ms.setActionHandler('seekforward', (d) => seek(position.value + (d?.seekOffset ?? 10)))
|
||||||
|
ms.setActionHandler('seekto', (d) => {
|
||||||
|
if (d.seekTime != null) seek(d.seekTime)
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
/* 个别动作类型 WebView 不支持时忽略 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSmtcState(state: 'playing' | 'paused') {
|
||||||
|
if ('mediaSession' in navigator) navigator.mediaSession.playbackState = state
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步播放进度到系统媒体浮层(SMTC 时间轴)。必须在时长已知后调用,
|
||||||
|
* 否则浮层只显示歌名没有进度条;position 越界会被 WebView 拒绝,做夹取。 */
|
||||||
|
function updateSmtcPosition() {
|
||||||
|
if (!('mediaSession' in navigator)) return
|
||||||
|
const d = duration.value
|
||||||
|
if (!isFinite(d) || d <= 0) return
|
||||||
|
try {
|
||||||
|
navigator.mediaSession.setPositionState({
|
||||||
|
duration: d,
|
||||||
|
playbackRate: audioEl?.playbackRate || 1,
|
||||||
|
position: Math.min(Math.max(position.value, 0), d)
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
/* 元数据切换瞬间的非法参数(如新歌时长未就绪)忽略 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function computeBuffered(a: HTMLAudioElement): number {
|
function computeBuffered(a: HTMLAudioElement): number {
|
||||||
const d = a.duration
|
const d = a.duration
|
||||||
if (!isFinite(d) || d <= 0 || a.buffered.length === 0) return 0
|
if (!isFinite(d) || d <= 0 || a.buffered.length === 0) return 0
|
||||||
@@ -204,28 +298,47 @@ export const useFeiniuStore = defineStore('feiniu', () => {
|
|||||||
a.addEventListener('loadedmetadata', () => {
|
a.addEventListener('loadedmetadata', () => {
|
||||||
duration.value = isFinite(a.duration) ? a.duration : 0
|
duration.value = isFinite(a.duration) ? a.duration : 0
|
||||||
bufferedPercent.value = computeBuffered(a)
|
bufferedPercent.value = computeBuffered(a)
|
||||||
|
updateSmtcPosition()
|
||||||
})
|
})
|
||||||
a.addEventListener('durationchange', () => {
|
a.addEventListener('durationchange', () => {
|
||||||
duration.value = isFinite(a.duration) ? a.duration : 0
|
duration.value = isFinite(a.duration) ? a.duration : 0
|
||||||
|
updateSmtcPosition()
|
||||||
})
|
})
|
||||||
a.addEventListener('timeupdate', () => (position.value = a.currentTime))
|
a.addEventListener('timeupdate', () => {
|
||||||
|
position.value = a.currentTime
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - smtcLastPosAt >= 1000) {
|
||||||
|
smtcLastPosAt = now
|
||||||
|
updateSmtcPosition()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
a.addEventListener('seeked', () => updateSmtcPosition())
|
||||||
a.addEventListener('progress', () => (bufferedPercent.value = computeBuffered(a)))
|
a.addEventListener('progress', () => (bufferedPercent.value = computeBuffered(a)))
|
||||||
a.addEventListener('waiting', () => (loadingPlay.value = true))
|
a.addEventListener('waiting', () => (loadingPlay.value = true))
|
||||||
a.addEventListener('canplay', () => (loadingPlay.value = false))
|
a.addEventListener('canplay', () => (loadingPlay.value = false))
|
||||||
a.addEventListener('playing', () => {
|
a.addEventListener('playing', () => {
|
||||||
loadingPlay.value = false
|
loadingPlay.value = false
|
||||||
playing.value = true
|
playing.value = true
|
||||||
|
setSmtcState('playing')
|
||||||
|
})
|
||||||
|
a.addEventListener('play', () => {
|
||||||
|
playing.value = true
|
||||||
|
setSmtcState('playing')
|
||||||
|
})
|
||||||
|
a.addEventListener('pause', () => {
|
||||||
|
playing.value = false
|
||||||
|
setSmtcState('paused')
|
||||||
})
|
})
|
||||||
a.addEventListener('play', () => (playing.value = true))
|
|
||||||
a.addEventListener('pause', () => (playing.value = false))
|
|
||||||
a.addEventListener('ended', () => onEnded())
|
a.addEventListener('ended', () => onEnded())
|
||||||
a.addEventListener('error', () => {
|
a.addEventListener('error', () => {
|
||||||
// 主动清空 src 时也会触发 error,此时不应报错
|
// 主动清空 src 时也会触发 error,此时不应报错
|
||||||
if (!a.getAttribute('src')) return
|
if (!a.getAttribute('src')) return
|
||||||
loadingPlay.value = false
|
loadingPlay.value = false
|
||||||
playing.value = false
|
playing.value = false
|
||||||
|
setSmtcState('paused')
|
||||||
playError.value = '音频加载失败:网络不可达或链接已失效'
|
playError.value = '音频加载失败:网络不可达或链接已失效'
|
||||||
})
|
})
|
||||||
|
setupMediaSessionHandlers(a)
|
||||||
audioEl = a
|
audioEl = a
|
||||||
}
|
}
|
||||||
return audioEl
|
return audioEl
|
||||||
@@ -238,6 +351,7 @@ export const useFeiniuStore = defineStore('feiniu', () => {
|
|||||||
position.value = 0
|
position.value = 0
|
||||||
duration.value = 0
|
duration.value = 0
|
||||||
bufferedPercent.value = 0
|
bufferedPercent.value = 0
|
||||||
|
setSmtcState('paused')
|
||||||
if (audioEl) {
|
if (audioEl) {
|
||||||
audioEl.pause()
|
audioEl.pause()
|
||||||
audioEl.removeAttribute('src')
|
audioEl.removeAttribute('src')
|
||||||
@@ -805,6 +919,7 @@ export const useFeiniuStore = defineStore('feiniu', () => {
|
|||||||
const t = current.value
|
const t = current.value
|
||||||
if (!t) return
|
if (!t) return
|
||||||
preview.value = null
|
preview.value = null
|
||||||
|
updateMediaSession(t)
|
||||||
if (t.source === 'feiniu') {
|
if (t.source === 'feiniu') {
|
||||||
void loadLyric(t.guid)
|
void loadLyric(t.guid)
|
||||||
} else {
|
} else {
|
||||||
@@ -872,6 +987,7 @@ export const useFeiniuStore = defineStore('feiniu', () => {
|
|||||||
const a = ensureAudio()
|
const a = ensureAudio()
|
||||||
a.pause()
|
a.pause()
|
||||||
preview.value = { ...item, source: 'preview' }
|
preview.value = { ...item, source: 'preview' }
|
||||||
|
updateMediaSession(preview.value)
|
||||||
lyricLines.value = []
|
lyricLines.value = []
|
||||||
lyricRaw = ''
|
lyricRaw = ''
|
||||||
lyricIdx = -1
|
lyricIdx = -1
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 778 KiB |
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
{"error":"internal","message":".NET number values such as positive and negative infinity cannot be written as valid JSON. To make it work when using 'JsonSerializer', consider specifying 'JsonNumberHandling.AllowNamedFloatingPointLiterals' (see https://docs.microsoft.com/dotnet/api/system.text.json.serialization.jsonnumberhandling)."}
|
|
||||||
Reference in New Issue
Block a user