feat: toggle next auto-update time on subscription card click and show update result feedback

This commit is contained in:
wonfen 2025-04-25 17:17:34 +08:00
parent bd3231bfa8
commit d6a79316a6
10 changed files with 391 additions and 43 deletions

View File

@ -28,6 +28,7 @@
- 系统代理守卫可检查意外设置变更并恢复 - 系统代理守卫可检查意外设置变更并恢复
- 定时自动订阅更新也能自动回退使用代理 - 定时自动订阅更新也能自动回退使用代理
- 订阅请求超时机制,防止订阅更新的时候卡死 - 订阅请求超时机制,防止订阅更新的时候卡死
- 订阅卡片点击时间可切换下次自动更新时间,自动更新触发后页面有明确的成功与否提示
#### 优化了: #### 优化了:
- 系统代理 Bypass 设置 - 系统代理 Bypass 设置

View File

@ -1,11 +1,12 @@
use super::CmdResult; use super::CmdResult;
use crate::{ use crate::{
config::{Config, IProfiles, PrfItem, PrfOption}, config::{Config, IProfiles, PrfItem, PrfOption},
core::{handle, tray::Tray, CoreManager}, core::{handle, timer::Timer, tray::Tray, CoreManager},
feat, logging, ret_err, feat, logging, ret_err,
utils::{dirs, help, logging::Type}, utils::{dirs, help, logging::Type},
wrap_err, wrap_err,
}; };
use tauri::Emitter;
/// 获取配置文件列表 /// 获取配置文件列表
#[tauri::command] #[tauri::command]
@ -45,7 +46,7 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
/// 更新配置文件 /// 更新配置文件
#[tauri::command] #[tauri::command]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult { pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
wrap_err!(feat::update_profile(index, option).await) wrap_err!(feat::update_profile(index, option, Some(true)).await)
} }
/// 删除配置文件 /// 删除配置文件
@ -211,7 +212,35 @@ pub async fn patch_profiles_config_by_profile_index(
/// 修改某个profile item的 /// 修改某个profile item的
#[tauri::command] #[tauri::command]
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult { pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
wrap_err!(Config::profiles().data().patch_item(index, profile))?; // 保存修改前检查是否有更新 update_interval
let update_interval_changed =
if let Ok(old_profile) = Config::profiles().latest().get_item(&index) {
let old_interval = old_profile.option.as_ref().and_then(|o| o.update_interval);
let new_interval = profile.option.as_ref().and_then(|o| o.update_interval);
old_interval != new_interval
} else {
false
};
// 保存修改
wrap_err!(Config::profiles().data().patch_item(index.clone(), profile))?;
// 如果更新间隔变更,异步刷新定时器
if update_interval_changed {
let index_clone = index.clone();
crate::process::AsyncHandler::spawn(move || async move {
logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器...");
if let Err(e) = crate::core::Timer::global().refresh() {
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
} else {
// 刷新成功后发送自定义事件,不触发配置重载
if let Some(window) = crate::core::handle::Handle::global().get_window() {
let _ = window.emit("verge://timer-updated", index_clone);
}
}
});
}
Ok(()) Ok(())
} }
@ -242,3 +271,11 @@ pub fn read_profile_file(index: String) -> CmdResult<String> {
let data = wrap_err!(item.read_file())?; let data = wrap_err!(item.read_file())?;
Ok(data) Ok(data)
} }
/// 获取下一次更新时间
#[tauri::command]
pub fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
let timer = Timer::global();
let next_time = timer.get_next_update_time(&uid);
Ok(next_time)
}

View File

@ -10,3 +10,4 @@ pub mod tray;
pub mod win_uwp; pub mod win_uwp;
pub use self::core::*; pub use self::core::*;
pub use self::timer::Timer;

View File

@ -1,6 +1,4 @@
use crate::{ use crate::{config::Config, feat, logging, logging_error, utils::logging::Type};
config::Config, core::CoreManager, feat, logging, logging_error, utils::logging::Type,
};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder}; use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -71,6 +69,25 @@ impl Timer {
return Err(e); return Err(e);
} }
let timer_map = self.timer_map.read();
logging!(
info,
Type::Timer,
"已注册的定时任务数量: {}",
timer_map.len()
);
for (uid, task) in timer_map.iter() {
logging!(
info,
Type::Timer,
"注册了定时任务 - uid={}, interval={}min, task_id={}",
uid,
task.interval_minutes,
task.task_id
);
}
let cur_timestamp = chrono::Local::now().timestamp(); let cur_timestamp = chrono::Local::now().timestamp();
// Collect profiles that need immediate update // Collect profiles that need immediate update
@ -83,6 +100,7 @@ impl Timer {
let uid = item.uid.as_ref()?; let uid = item.uid.as_ref()?;
if interval > 0 && cur_timestamp - updated >= interval * 60 { if interval > 0 && cur_timestamp - updated >= interval * 60 {
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
Some(uid.clone()) Some(uid.clone())
} else { } else {
None None
@ -95,12 +113,18 @@ impl Timer {
// Advance tasks outside of locks to minimize lock contention // Advance tasks outside of locks to minimize lock contention
if !profiles_to_update.is_empty() { if !profiles_to_update.is_empty() {
logging!(
info,
Type::Timer,
"需要立即更新的配置数量: {}",
profiles_to_update.len()
);
let timer_map = self.timer_map.read(); let timer_map = self.timer_map.read();
let delay_timer = self.delay_timer.write(); let delay_timer = self.delay_timer.write();
for uid in profiles_to_update { for uid in profiles_to_update {
if let Some(task) = timer_map.get(&uid) { if let Some(task) = timer_map.get(&uid) {
logging!(info, Type::Timer, "Advancing task for uid: {}", uid); logging!(info, Type::Timer, "立即执行任务: uid={}", uid);
if let Err(e) = delay_timer.advance_task(task.task_id) { if let Err(e) = delay_timer.advance_task(task.task_id) {
logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e); logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e);
} }
@ -210,6 +234,13 @@ impl Timer {
if let Some(option) = item.option.as_ref() { if let Some(option) = item.option.as_ref() {
if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) { if let (Some(interval), Some(uid)) = (option.update_interval, &item.uid) {
if interval > 0 { if interval > 0 {
logging!(
debug,
Type::Timer,
"找到定时更新配置: uid={}, interval={}min",
uid,
interval
);
new_map.insert(uid.clone(), interval); new_map.insert(uid.clone(), interval);
} }
} }
@ -217,6 +248,12 @@ impl Timer {
} }
} }
logging!(
debug,
Type::Timer,
"生成的定时更新配置数量: {}",
new_map.len()
);
new_map new_map
} }
@ -227,20 +264,36 @@ impl Timer {
// Read lock for comparing current state // Read lock for comparing current state
let timer_map = self.timer_map.read(); let timer_map = self.timer_map.read();
logging!(
debug,
Type::Timer,
"当前 timer_map 大小: {}",
timer_map.len()
);
// Find tasks to modify or delete // Find tasks to modify or delete
for (uid, task) in timer_map.iter() { for (uid, task) in timer_map.iter() {
match new_map.get(uid) { match new_map.get(uid) {
Some(&interval) if interval != task.interval_minutes => { Some(&interval) if interval != task.interval_minutes => {
// Task exists but interval changed // Task exists but interval changed
logging!(
debug,
Type::Timer,
"定时任务间隔变更: uid={}, 旧={}, 新={}",
uid,
task.interval_minutes,
interval
);
diff_map.insert(uid.clone(), DiffFlag::Mod(task.task_id, interval)); diff_map.insert(uid.clone(), DiffFlag::Mod(task.task_id, interval));
} }
None => { None => {
// Task no longer needed // Task no longer needed
logging!(debug, Type::Timer, "定时任务已删除: uid={}", uid);
diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id)); diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id));
} }
_ => { _ => {
// Task exists with same interval, no change needed // Task exists with same interval, no change needed
logging!(debug, Type::Timer, "定时任务保持不变: uid={}", uid);
} }
} }
} }
@ -250,6 +303,13 @@ impl Timer {
for (uid, &interval) in new_map.iter() { for (uid, &interval) in new_map.iter() {
if !timer_map.contains_key(uid) { if !timer_map.contains_key(uid) {
logging!(
debug,
Type::Timer,
"新增定时任务: uid={}, interval={}min",
uid,
interval
);
diff_map.insert(uid.clone(), DiffFlag::Add(next_id, interval)); diff_map.insert(uid.clone(), DiffFlag::Add(next_id, interval));
next_id += 1; next_id += 1;
} }
@ -260,6 +320,7 @@ impl Timer {
*self.timer_count.lock() = next_id; *self.timer_count.lock() = next_id;
} }
logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len());
diff_map diff_map
} }
@ -300,42 +361,126 @@ impl Timer {
Ok(()) Ok(())
} }
/// Get next update time for a profile
pub fn get_next_update_time(&self, uid: &str) -> Option<i64> {
logging!(info, Type::Timer, "获取下次更新时间uid={}", uid);
let timer_map = self.timer_map.read();
let task = match timer_map.get(uid) {
Some(t) => t,
None => {
logging!(warn, Type::Timer, "找不到对应的定时任务uid={}", uid);
return None;
}
};
// Get the profile updated timestamp
let profiles_config = Config::profiles();
let profiles = profiles_config.latest();
let items = match profiles.get_items() {
Some(i) => i,
None => {
logging!(warn, Type::Timer, "获取配置列表失败");
return None;
}
};
// 修复类型比较,使用字符串值比较而不是引用比较
let profile = match items
.iter()
.find(|item| item.uid.as_ref().map(|u| u.as_str()) == Some(uid))
{
Some(p) => p,
None => {
logging!(warn, Type::Timer, "找不到对应的配置uid={}", uid);
return None;
}
};
let updated = profile.updated.unwrap_or(0) as i64;
// Calculate next update time
if updated > 0 && task.interval_minutes > 0 {
let next_time = updated + (task.interval_minutes as i64 * 60);
logging!(
info,
Type::Timer,
"计算得到下次更新时间: {}, uid={}",
next_time,
uid
);
Some(next_time)
} else {
logging!(
warn,
Type::Timer,
"更新时间或间隔无效updated={}, interval={}",
updated,
task.interval_minutes
);
None
}
}
/// Emit update events for frontend notification
fn emit_update_event(_uid: &str, _is_start: bool) {
// 当feature="verge-dev"或"default"时才启用此功能
#[cfg(any(feature = "verge-dev", feature = "default"))]
{
use serde_json::json;
use tauri::Emitter;
let event_name = if _is_start {
"profile-update-started"
} else {
"profile-update-completed"
};
if let Some(window) = super::handle::Handle::global().get_window() {
let _ = window.emit(event_name, json!({ "uid": _uid }));
}
}
}
/// Async task with better error handling and logging /// Async task with better error handling and logging
async fn async_task(uid: String) { async fn async_task(uid: String) {
let task_start = std::time::Instant::now(); let task_start = std::time::Instant::now();
logging!(info, Type::Timer, "Running timer task for profile: {}", uid); logging!(info, Type::Timer, "Running timer task for profile: {}", uid);
// Update profile // Emit start event
let profile_result = feat::update_profile(uid.clone(), None).await; Self::emit_update_event(&uid, true);
// 检查是否是当前激活的配置文件
let is_current = Config::profiles().latest().current.as_ref() == Some(&uid);
logging!(
info,
Type::Timer,
"配置 {} 是否为当前激活配置: {}",
uid,
is_current
);
// Update profile - 由update_profile函数自动处理是否需要刷新UI
let profile_result = feat::update_profile(uid.clone(), None, Some(is_current)).await;
match profile_result { match profile_result {
Ok(_) => { Ok(_) => {
// Update configuration let duration = task_start.elapsed().as_millis();
match CoreManager::global().update_config().await { logging!(
Ok(_) => { info,
let duration = task_start.elapsed().as_millis(); Type::Timer,
logging!( "Timer task completed successfully for uid: {} (took {}ms)",
info, uid,
Type::Timer, duration
"Timer task completed successfully for uid: {} (took {}ms)", );
uid,
duration
);
}
Err(e) => {
logging_error!(
Type::Timer,
"Failed to refresh config after profile update for uid {}: {}",
uid,
e
);
}
}
} }
Err(e) => { Err(e) => {
logging_error!(Type::Timer, "Failed to update profile uid {}: {}", uid, e); logging_error!(Type::Timer, "Failed to update profile uid {}: {}", uid, e);
} }
} }
// Emit completed event
Self::emit_update_event(&uid, false);
} }
} }

View File

@ -23,8 +23,14 @@ pub fn toggle_proxy_profile(profile_index: String) {
/// Update a profile /// Update a profile
/// If updating current profile, activate it /// If updating current profile, activate it
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> { /// auto_refresh: 是否自动更新配置和刷新前端
pub async fn update_profile(
uid: String,
option: Option<PrfOption>,
auto_refresh: Option<bool>,
) -> Result<()> {
println!("[订阅更新] 开始更新订阅 {}", uid); println!("[订阅更新] 开始更新订阅 {}", uid);
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true保持兼容性
let url_opt = { let url_opt = {
let profiles = Config::profiles(); let profiles = Config::profiles();
@ -63,7 +69,7 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
let is_current = Some(uid.clone()) == profiles.get_current(); let is_current = Some(uid.clone()) == profiles.get_current();
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current); println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
is_current is_current && auto_refresh
} }
Err(err) => { Err(err) => {
// 首次更新失败尝试使用Clash代理 // 首次更新失败尝试使用Clash代理
@ -105,7 +111,7 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
let is_current = Some(uid.clone()) == profiles.get_current(); let is_current = Some(uid.clone()) == profiles.get_current();
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current); println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
is_current is_current && auto_refresh
} }
Err(retry_err) => { Err(retry_err) => {
println!("[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err); println!("[订阅更新] 使用Clash代理更新仍然失败: {}", retry_err);
@ -119,7 +125,7 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
} }
} }
} }
None => true, None => auto_refresh,
}; };
if should_update { if should_update {

View File

@ -213,6 +213,7 @@ pub fn run() {
cmd::delete_profile, cmd::delete_profile,
cmd::read_profile_file, cmd::read_profile_file,
cmd::save_profile_file, cmd::save_profile_file,
cmd::get_next_update_time,
// script validation // script validation
cmd::script_validate_notice, cmd::script_validate_notice,
cmd::validate_script_file, cmd::validate_script_file,

View File

@ -22,6 +22,7 @@ import {
readProfileFile, readProfileFile,
updateProfile, updateProfile,
saveProfileFile, saveProfileFile,
getNextUpdateTime,
} from "@/services/cmds"; } from "@/services/cmds";
import { Notice } from "@/components/base"; import { Notice } from "@/components/base";
import { GroupsEditorViewer } from "@/components/profile/groups-editor-viewer"; import { GroupsEditorViewer } from "@/components/profile/groups-editor-viewer";
@ -65,9 +66,107 @@ export const ProfileItem = (props: Props) => {
const [position, setPosition] = useState({ left: 0, top: 0 }); const [position, setPosition] = useState({ left: 0, top: 0 });
const loadingCache = useLoadingCache(); const loadingCache = useLoadingCache();
const setLoadingCache = useSetLoadingCache(); const setLoadingCache = useSetLoadingCache();
// 新增状态:是否显示下次更新时间
const [showNextUpdate, setShowNextUpdate] = useState(false);
const [nextUpdateTime, setNextUpdateTime] = useState("");
const { uid, name = "Profile", extra, updated = 0, option } = itemData; const { uid, name = "Profile", extra, updated = 0, option } = itemData;
// 获取下次更新时间的函数
const fetchNextUpdateTime = useLockFn(async (forceRefresh = false) => {
if (itemData.option?.update_interval && itemData.option.update_interval > 0) {
try {
console.log(`尝试获取配置 ${itemData.uid} 的下次更新时间`);
// 如果需要强制刷新先触发Timer.refresh()
if (forceRefresh) {
// 这里可以通过一个新的API来触发刷新但目前我们依赖patch_profile中的刷新
console.log(`强制刷新定时器任务`);
}
const nextUpdate = await getNextUpdateTime(itemData.uid);
console.log(`获取到下次更新时间结果:`, nextUpdate);
if (nextUpdate) {
const nextUpdateDate = dayjs(nextUpdate * 1000);
const now = dayjs();
// 如果已经过期,显示"更新失败"
if (nextUpdateDate.isBefore(now)) {
setNextUpdateTime(t("Last Update failed"));
} else {
// 否则显示剩余时间
const diffMinutes = nextUpdateDate.diff(now, 'minute');
if (diffMinutes < 60) {
if (diffMinutes <= 0) {
setNextUpdateTime(`${t("Next Up")} <1m`);
} else {
setNextUpdateTime(`${t("Next Up")} ${diffMinutes}m`);
}
} else {
const hours = Math.floor(diffMinutes / 60);
const mins = diffMinutes % 60;
setNextUpdateTime(`${t("Next Up")} ${hours}h ${mins}m`);
}
}
} else {
console.log(`返回的下次更新时间为空`);
setNextUpdateTime(t("No schedule"));
}
} catch (err) {
console.error(`获取下次更新时间出错:`, err);
setNextUpdateTime(t("Unknown"));
}
} else {
console.log(`该配置未设置更新间隔或间隔为0`);
setNextUpdateTime(t("Auto update disabled"));
}
});
// 切换显示模式的函数
const toggleUpdateTimeDisplay = (e: React.MouseEvent) => {
e.stopPropagation();
if (!showNextUpdate) {
fetchNextUpdateTime();
}
setShowNextUpdate(!showNextUpdate);
};
// 当组件加载或更新间隔变化时更新下次更新时间
useEffect(() => {
if (showNextUpdate) {
fetchNextUpdateTime();
}
}, [showNextUpdate, itemData.option?.update_interval, updated]);
// 订阅定时器更新事件
useEffect(() => {
// 处理定时器更新事件 - 这个事件专门用于通知定时器变更
const handleTimerUpdate = (event: any) => {
const updatedUid = event.payload as string;
// 只有当更新的是当前配置时才刷新显示
if (updatedUid === itemData.uid && showNextUpdate) {
console.log(`收到定时器更新事件: uid=${updatedUid}`);
setTimeout(() => {
fetchNextUpdateTime(true);
}, 1000);
}
};
// 只注册定时器更新事件监听
window.addEventListener('verge://timer-updated', handleTimerUpdate as EventListener);
return () => {
// 清理事件监听
window.removeEventListener('verge://timer-updated', handleTimerUpdate as EventListener);
};
}, [showNextUpdate, itemData.uid]);
// local file mode // local file mode
// remote file mode // remote file mode
// remote file mode // remote file mode
@ -303,6 +402,35 @@ export const ProfileItem = (props: Props) => {
justifyContent: "space-between", justifyContent: "space-between",
}; };
// 监听自动更新事件
useEffect(() => {
const handleUpdateStarted = (event: CustomEvent) => {
if (event.detail.uid === itemData.uid) {
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
}
};
const handleUpdateCompleted = (event: CustomEvent) => {
if (event.detail.uid === itemData.uid) {
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: false }));
// 更新完成后刷新显示
if (showNextUpdate) {
fetchNextUpdateTime();
}
}
};
// 注册事件监听
window.addEventListener('profile-update-started', handleUpdateStarted as EventListener);
window.addEventListener('profile-update-completed', handleUpdateCompleted as EventListener);
return () => {
// 清理事件监听
window.removeEventListener('profile-update-started', handleUpdateStarted as EventListener);
window.removeEventListener('profile-update-completed', handleUpdateCompleted as EventListener);
};
}, [itemData.uid, showNextUpdate]);
return ( return (
<Box <Box
sx={{ sx={{
@ -413,15 +541,30 @@ export const ProfileItem = (props: Props) => {
) )
)} )}
{hasUrl && ( {hasUrl && (
<Typography <Box sx={{ display: "flex", justifyContent: "flex-end", ml: "auto" }}>
noWrap <Typography
flex="1 0 auto" noWrap
fontSize={14} component="span"
textAlign="right" fontSize={14}
title={`${t("Update Time")}: ${parseExpire(updated)}`} textAlign="right"
> title={showNextUpdate ? t("Click to show last update time") : `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`}
{updated > 0 ? dayjs(updated * 1000).fromNow() : ""} sx={{
</Typography> cursor: "pointer",
display: "inline-block",
borderBottom: "1px dashed transparent",
transition: "all 0.2s",
"&:hover": {
borderBottomColor: "primary.main",
color: "primary.main",
}
}}
onClick={toggleUpdateTimeDisplay}
>
{showNextUpdate
? nextUpdateTime
: (updated > 0 ? dayjs(updated * 1000).fromNow() : "")}
</Typography>
</Box>
)} )}
</> </>
} }

View File

@ -563,6 +563,11 @@
"Administrator + Service Mode": "Admin + Service Mode", "Administrator + Service Mode": "Admin + Service Mode",
"Last Check Update": "Last Check Update", "Last Check Update": "Last Check Update",
"Click to import subscription": "Click to import subscription", "Click to import subscription": "Click to import subscription",
"Last Update failed": "Last Update failed",
"Next Up": "Next Up",
"No schedule": "No schedule",
"Unknown": "Unknown",
"Auto update disabled": "Auto update disabled",
"Update subscription successfully": "Update subscription successfully", "Update subscription successfully": "Update subscription successfully",
"Update failed, retrying with Clash proxy...": "Update failed, retrying with Clash proxy...", "Update failed, retrying with Clash proxy...": "Update failed, retrying with Clash proxy...",
"Update with Clash proxy successfully": "Update with Clash proxy successfully", "Update with Clash proxy successfully": "Update with Clash proxy successfully",

View File

@ -563,6 +563,11 @@
"Administrator + Service Mode": "管理员 + 服务模式", "Administrator + Service Mode": "管理员 + 服务模式",
"Last Check Update": "最后检查更新", "Last Check Update": "最后检查更新",
"Click to import subscription": "点击导入订阅", "Click to import subscription": "点击导入订阅",
"Last Update failed": "上次更新失败",
"Next Up": "下次更新",
"No schedule": "没有计划",
"Unknown": "未知",
"Auto update disabled": "自动更新已禁用",
"Update subscription successfully": "订阅更新成功", "Update subscription successfully": "订阅更新成功",
"Update failed, retrying with Clash proxy...": "订阅更新失败,尝试使用 Clash 代理更新", "Update failed, retrying with Clash proxy...": "订阅更新失败,尝试使用 Clash 代理更新",
"Update with Clash proxy successfully": "使用 Clash 代理更新成功", "Update with Clash proxy successfully": "使用 Clash 代理更新成功",

View File

@ -359,3 +359,7 @@ export const isAdmin = async () => {
return false; return false;
} }
}; };
export async function getNextUpdateTime(uid: string) {
return invoke<number | null>("get_next_update_time", { uid });
}