From c59fab881afb2f33deeddb659613166682a5932d Mon Sep 17 00:00:00 2001 From: toorux Date: Mon, 5 Oct 2026 12:53:07 +0800 Subject: [PATCH] Release 0.4.3-preview.1 with notification inbox and headset interaction fixes --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 + README.zh-CN.md | 2 + docs/developer-guide/sdk.md | 33 +- docs/images/plugin-library.png | Bin 0 -> 187649 bytes docs/user-guide/basic-usage.md | 17 +- docs/zh-CN/developer-guide/sdk.md | 33 +- docs/zh-CN/user-guide/basic-usage.md | 21 +- examples/showcase/page.tsx | 2 +- native/host.cpp | 76 +- native/notification_badge.h | 28 + native/notification_geometry.h | 49 +- native/plugin_navigation.h | 21 + package-lock.json | 27 + package.json | 1 + sdk/src/index.tsx | 6 +- src/model.rs | 112 ++- src/service.rs | 882 ++++++++++++++++++++++-- src/session.rs | 40 +- src/tests.rs | 3 + src/update.rs | 2 +- tests/browser_probe.cpp | 3 +- tests/notification_badge.cpp | 15 + tests/notification_geometry.cpp | 45 +- tests/notification_settings_fixture.tsx | 92 +++ tests/notifications_fixture.tsx | 104 +++ tests/plugin_navigation.cpp | 13 + tests/system_updates_fixture.tsx | 15 +- tools/package-release.sh | 1 + tools/plugin-dev.mjs | 2 +- tools/test-install-review.sh | 2 + tools/test-native.sh | 2 +- ui/locales/en-US.json | 49 +- ui/locales/zh-CN.json | 49 +- ui/src/language.tsx | 4 +- ui/src/main.tsx | 28 +- ui/src/notification-settings.tsx | 71 ++ ui/src/notifications.tsx | 49 ++ ui/src/style.css | 49 ++ ui/src/updates.tsx | 13 + 41 files changed, 1849 insertions(+), 118 deletions(-) create mode 100644 docs/images/plugin-library.png create mode 100644 native/notification_badge.h create mode 100644 native/plugin_navigation.h create mode 100644 tests/notification_badge.cpp create mode 100644 tests/notification_settings_fixture.tsx create mode 100644 tests/notifications_fixture.tsx create mode 100644 tests/plugin_navigation.cpp create mode 100644 ui/src/notification-settings.tsx create mode 100644 ui/src/notifications.tsx diff --git a/Cargo.lock b/Cargo.lock index 75795cd..871082e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,7 +291,7 @@ dependencies = [ [[package]] name = "framely" -version = "0.4.2-preview.12" +version = "0.4.3-preview.1" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 316d2a5..4f3fe21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "framely" -version = "0.4.2-preview.12" +version = "0.4.3-preview.1" edition = "2021" license = "AGPL-3.0-only" diff --git a/README.md b/README.md index 5850ff8..58eee17 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ A React plugin manager for Steam Frame, with a Rust core service, a separate CEF/OpenVR host, a network management panel, and a desktop installer. Manage plugins, sources, windows, notifications, and updates. +![Framely plugin library](docs/images/plugin-library.png) + ## Installation The target device is a Linux ARM64 Steam Frame. Download the `Framely Installer` for your computer from [Releases](https://github.com/SteamFramelyHomebrew/framely/releases), connect to the device, and install Framely. Alternatively, run this in the Frame SSH terminal to install the latest stable release: diff --git a/README.zh-CN.md b/README.zh-CN.md index fe7525f..ec956cc 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -9,6 +9,8 @@ Steam Frame 的 React 插件管理器:Rust 核心服务、独立 CEF/OpenVR 宿主、网络管理面板和电脑端安装器。可管理插件、来源、窗口、通知和更新。 +![Framely 插件库](docs/images/plugin-library.png) + ## 安装 目标为 Linux ARM64 Steam Frame。电脑端从 [Releases](https://github.com/SteamFramelyHomebrew/framely/releases) 下载对应平台的 `Framely Installer`,连接设备后安装本体。也可在 Frame 的 SSH 终端安装最新正式版: diff --git a/docs/developer-guide/sdk.md b/docs/developer-guide/sdk.md index c136416..4dbbac5 100644 --- a/docs/developer-guide/sdk.md +++ b/docs/developer-guide/sdk.md @@ -25,7 +25,8 @@ registerPlugin({QuickPage, windows: {main: MainWindow}}); | `framely.windows.open(key)` | Declared key; asynchronous result | Open a declared window | | `framely.windows.close(key)` | Key; asynchronous result | Close a window without disabling the backend | | `framely.notifications.send(notification)` | Notification; asynchronous result | Send or replace by ID | -| `framely.notifications.remove(id)` | Notification ID; asynchronous result | Withdraw notification | +| `framely.notifications.remove(id)` | Notification ID; asynchronous result | Close the popup and remove the inbox copy | +| `framely.notifications.dismiss(id)` | Notification ID; asynchronous result | Close the popup, retaining the inbox copy | | `framely.dependencies()` | `Promise` | Query declared dependency state | | `framely.language.get()` | `Promise<{preference, language}>` | Language preference and effective language | | `framely.onEvent(callback)` | Unsubscribe function | Receive forwarded events | @@ -43,14 +44,38 @@ try { ```tsx await framely.notifications.send({ - id: 'task', title: 'Done', body: 'File processed', durationMs: 8000, + id: 'task', title: 'Done', body: 'File processed', inbox: true, durationMs: 8000, actions: [{id: 'open', label: 'Open', icon: '↗'}], }); ``` -`id`, `title`, `body` are required. Optional `image` accepts PNG/JPEG data URLs or HTTPS, up to a 1 MiB string. At most three actions, each with `id`, `label`, `icon`. `durationMs` is 1000–60000; omission defaults to 8000 ms. Notification/action IDs follow core ID rules; action IDs must be unique within a notification. Title is at most 160 bytes, body 4096, nonempty action label 80, and action icon 16. Each plugin may send at most ten notifications per ten seconds; the global queue permits 64 concurrent notifications. Updating an existing ID does not occupy another slot. +`id`, `title`, `body` are required. `inbox` defaults to `false`: with popups enabled, only notifications explicitly sent with `inbox: true` appear in the quick panel's Notifications tab. Inbox messages survive popup expiry, closing the popup, plugin disable/restart, and Framely restart. They are removed by an action's policy, explicit removal, or plugin uninstall. Restored inbox messages do not replay as popups. -Buttons invoke backend `notification.action` with `{id, action}` and forward a page event of the same name. UI-only plugins can listen for the event. Reuse an ID to update; remove it to withdraw. +| Field | Default | Behavior | +| --- | --- | --- | +| `inbox` | `false` | Save the message in the inbox; user popup settings may also require saving it | +| `durationMs` | `8000` | Popup countdown in milliseconds; `1000-60000`, or `0` to stay open until dismissed | +| `actions[].closeOnClick` | `true` | Close the popup after this action succeeds | +| `actions[].removeFromInboxOnClick` | `true` | Remove the inbox copy after this action succeeds | + +User notification settings take priority over sender preferences. If the global popup switch or this plugin's popup switch is off, Framely suppresses the popup and saves the message in the inbox even when `inbox` is false. Reading the inbox clears the launcher unread badge but leaves messages and actions available. Plugins cannot override popup or badge settings. + +The two action policies are independent. For a status button that keeps the message, set both to `false`: + +```tsx +await framely.notifications.send({ + id: 'capture', title: 'Recording', body: 'Recording is in progress.', + inbox: true, durationMs: 0, + actions: [{id: 'status', label: 'View status', + closeOnClick: false, removeFromInboxOnClick: false}], +}); +``` + +Popups display the remaining auto-close countdown; inbox entries show their received time. Closing a popup keeps its saved inbox copy. `remove(id)` withdraws both copies; `dismiss(id)` closes only the popup. Reuse the same ID to replace a message and restart its countdown. Failed actions leave the message available for retry. Buttons use their text `label`; the legacy `icon` string remains optional for compatibility. + +Optional `image` accepts PNG/JPEG data URLs or HTTPS, up to a 1 MiB string. At most three actions, each with required `id` and `label`. Notification/action IDs follow core ID rules; action IDs must be unique within a notification. Title is at most 160 bytes, body 4096, nonempty action label 80, and optional action icon 16. Each plugin may send at most ten notifications per ten seconds; the global queue permits 64 simultaneous popups and 256 saved inbox messages. When full, sending a new message fails; remove older messages first. Replacing an existing ID does not occupy another slot. + +Buttons invoke backend `notification.action` with `{id, action}` and forward a page event of the same name after a successful backend response. UI-only plugins can listen for the event. A disabled plugin's saved messages remain readable and removable, but their action buttons are disabled until it is enabled again. ## React hooks diff --git a/docs/images/plugin-library.png b/docs/images/plugin-library.png new file mode 100644 index 0000000000000000000000000000000000000000..3628f01114498aea30c0c0ee03f4e3d63ea6f386 GIT binary patch literal 187649 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

oi0Ro@Yydvyqs;EnY7HL??vC$8#(!XsR`mzk_r+ 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

每个按钮都调用正式 SDK。

setText(e.target.value)}/>
{result}
{Array.from({length:12},(_,i)=>

测试条目 {i+1}

)}
} +function Page(){const [text,setText]=useState('');const [result,setResult]=useState('');const [count,setCount]=useState(0);async function run(f:()=>Promise){try{setResult(JSON.stringify(await f(),null,2));}catch(e){setResult(String(e));}}return <>
每个按钮都调用正式 SDK。

setText(e.target.value)}/>
{result}
{Array.from({length:12},(_,i)=>

测试条目 {i+1}

)}
} registerPlugin({QuickPage:Page,WindowPage:Page}); diff --git a/native/host.cpp b/native/host.cpp index c9d7456..5ef7ce6 100644 --- a/native/host.cpp +++ b/native/host.cpp @@ -11,10 +11,12 @@ #include "overlay_placement.h" #include "render_resolution.h" #include "notification_geometry.h" +#include "notification_badge.h" #include "keyboard_state.h" #include "keyboard_input.h" #include "brand_mask.h" #include "ui_visibility.h" +#include "plugin_navigation.h" #include #include #include @@ -40,7 +42,9 @@ static vr::IVRSystem* vr_system=nullptr; static Display* xdisplay=nullptr; static GLXContext glcontext=nullptr; static GLXPbuffer glsurface=0; +static std::atomic notification_badge_count{0}; static bool menu_open=false,keyboard_open=false,keyboard_close_pending=false; +static float notification_curve_radius=1.3f; static bool keyboard_toolbar=false,keyboard_chars=false,keyboard_done=false; static MenuButtonInput menu_button; static cef_frame_t* keyboard_frame=nullptr; @@ -59,8 +63,8 @@ template struct Handler { }; struct View { std::string key,plugin,title;int width=600,height=840,render_scale=1;bool dashboard=false,shown=false,dirty=false,closing=false,closed=false; - OverlayPlacement placement;std::chrono::steady_clock::time_point haptic_at{}; - bool active_seen=false;int browser_id=0;cef_browser_t* browser=nullptr;cef_browser_host_t* host=nullptr; + OverlayPlacement placement;NotificationClick notification_click;std::chrono::steady_clock::time_point haptic_at{}; + bool active_seen=false,notification_content=false;int browser_id=0;cef_browser_t* browser=nullptr;cef_browser_host_t* host=nullptr; vr::VROverlayHandle_t overlay=0,thumbnail=0;GLuint textures[2]{};int texture_slot=0;bool texture_ready[2]{};std::vector pixels; }; static std::map> views; @@ -99,7 +103,7 @@ static json poll_http(int port){int fd=socket(AF_INET,SOCK_STREAM,0);if(fd<0)thr // Keep a wider curved mesh around the narrow button; transparent margins never hit input. static constexpr int dock_canvas_width=640,dock_canvas_height=128,dock_icon_left=256; static std::vector dock_icon(bool hover,bool active){ - auto source=icon(hover,active);std::vector canvas(dock_canvas_width*dock_canvas_height*4,0); + auto source=icon(hover,active);notification_badge(source,notification_badge_count.load());std::vector canvas(dock_canvas_width*dock_canvas_height*4,0); for(int row=0;rowSetOverlayCursorPositionOverride(overlay,&position);} else if(event.eventType==vr::VREvent_FocusLeave)overlays->ClearOverlayCursorPositionOverride(overlay); } -static void show(View& v,bool shown){if(v.closing)return;if(v.key=="notifications")overlays->SetOverlayInputMethod(v.overlay,shown?vr::VROverlayInputMethod_Mouse:vr::VROverlayInputMethod_None);if(v.key=="menu"&&shown!=v.shown)overlays->SetOverlayFlag(v.overlay,vr::VROverlayFlags_WantsModalBehavior,shown&&!(keyboard_open&&keyboard_view=="menu"));bool actual=overlays->IsOverlayVisible(v.overlay);if(shown!=actual){auto e=shown?overlays->ShowOverlay(v.overlay):overlays->HideOverlay(v.overlay);if(e){std::cerr<<"Overlay visibility failed "<ClearOverlayCursorPositionOverride(v.overlay);if(v.key!="notifications")v.host->was_hidden(v.host,!shown);v.shown=shown;if(shown)v.host->invalidate(v.host,PET_VIEW);}} -static void close_keyboard(bool hide=true){if(keyboard_open&&hide)overlays->HideKeyboard();keyboard_open=false;keyboard_close_pending=false;keyboard_toolbar=false;keyboard_chars=false;keyboard_done=false;keyboard_target=0;keyboard_menu.end(std::chrono::steady_clock::now());if(keyboard_view=="menu"&&views.count("menu")&&!views.at("menu")->closing)overlays->SetOverlayFlag(views.at("menu")->overlay,vr::VROverlayFlags_WantsModalBehavior,views.at("menu")->shown);if(keyboard_frame){keyboard_frame->base.release(&keyboard_frame->base);keyboard_frame=nullptr;}keyboard_view.clear();} +static void update_menu_modal(){ + if(!views.count("menu"))return; + auto& menu=*views.at("menu"); + bool toast=views.count("notifications")&&views.at("notifications")->shown&&!views.at("notifications")->closing; + // A modal quick menu must not monopolize the laser while a toast needs input. + overlays->SetOverlayFlag(menu.overlay,vr::VROverlayFlags_WantsModalBehavior,menu.shown&&!toast&&!(keyboard_open&&keyboard_view=="menu")); +} +static void show(View& v,bool shown){ + if(v.closing)return; + bool actual=overlays->IsOverlayVisible(v.overlay); + if(v.key=="notifications"&&(shown!=v.shown||shown!=actual)){ + auto e=overlays->SetOverlayInputMethod(v.overlay,shown?vr::VROverlayInputMethod_Mouse:vr::VROverlayInputMethod_None); + if(!e)e=overlays->SetOverlayFlag(v.overlay,vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible,shown); + if(e){std::cerr<<"Notification input state failed "<SetOverlayInputMethod(v.overlay,vr::VROverlayInputMethod_None);} + } + if(shown!=actual){auto e=shown?overlays->ShowOverlay(v.overlay):overlays->HideOverlay(v.overlay);if(e){std::cerr<<"Overlay visibility failed "<ClearOverlayCursorPositionOverride(v.overlay);if(v.key=="notifications"&&v.notification_click.active){cef_mouse_event_t outside{};outside.x=outside.y=-1;v.host->send_mouse_click_event(v.host,&outside,MBT_LEFT,1,1);v.notification_click.clear();}} + if(v.key!="notifications")v.host->was_hidden(v.host,!shown); + v.shown=shown;if(shown)v.host->invalidate(v.host,PET_VIEW); + if(v.key=="menu"||v.key=="notifications")update_menu_modal(); + } +} +static void close_keyboard(bool hide=true){if(keyboard_open&&hide)overlays->HideKeyboard();keyboard_open=false;keyboard_close_pending=false;keyboard_toolbar=false;keyboard_chars=false;keyboard_done=false;keyboard_target=0;keyboard_menu.end(std::chrono::steady_clock::now());if(keyboard_view=="menu")update_menu_modal();if(keyboard_frame){keyboard_frame->base.release(&keyboard_frame->base);keyboard_frame=nullptr;}keyboard_view.clear();} static bool keyboard_event(const vr::VREvent_t& e,vr::VROverlayHandle_t source_overlay=0){ // The runtime toolbar opens the keyboard without a host.keyboard request. if(e.eventType==vr::VREvent_KeyboardOpened_Global){ @@ -144,11 +170,23 @@ static bool keyboard_event(const vr::VREvent_t& e,vr::VROverlayHandle_t source_o } static void close_view(View& v){if(v.closing)return;if(keyboard_view==v.key)close_keyboard();v.closing=true;if(v.key=="menu")overlays->SetOverlayFlag(v.overlay,vr::VROverlayFlags_WantsModalBehavior,false);overlays->ClearOverlayCursorPositionOverride(v.overlay);overlays->HideOverlay(v.overlay);v.host->close_browser(v.host,1);} static void anchor_to_head(View& v,float width,float x=0,float y=0,float z=-1.35f){vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount]{};vr_system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding,0,poses,vr::k_unMaxTrackedDeviceCount);if(!poses[0].bPoseIsValid)return;auto m=poses[0].mDeviceToAbsoluteTracking;for(int r=0;r<3;r++)m.m[r][3]+=m.m[r][0]*x+m.m[r][1]*y+m.m[r][2]*z;overlays->SetOverlayWidthInMeters(v.overlay,width);overlays->SetOverlayTransformAbsolute(v.overlay,vr::TrackingUniverseStanding,&m);} +static void anchor_notification(View& v){ + // Let the compositor follow the HMD's render pose; a captured standing-space + // transform leaves the toast behind when the user turns or moves their head. + vr::HmdMatrix34_t head{{{1,0,0,0},{0,1,0,0},{0,0,1,0}}}; + NotificationPlacement toast{}; + if(!notification_placement(head,notification_curve_radius,toast))return; + auto error=overlays->SetOverlayWidthInMeters(v.overlay,toast.width); + if(!error)error=overlays->SetOverlayTransformTrackedDeviceRelative(v.overlay,vr::k_unTrackedDeviceIndex_Hmd,&toast.transform); + if(error){std::cerr<<"Notification head attachment failed "<ShowDashboard(key.c_str());else if(key!="notifications")show(v,true);return v;} if(views.size()>=34)throw std::runtime_error("Window limit exceeded");auto v=std::make_unique();v->key=key;v->plugin=plugin;v->title=title;v->dashboard=dock;v->width=width;v->height=height;v->render_scale=key=="framely.manager"?4:(key=="menu"||key=="notifications")?2:1; auto error=dock?overlays->CreateDashboardOverlay(key.c_str(),title.c_str(),&v->overlay,&v->thumbnail):overlays->CreateOverlay(key.c_str(),title.c_str(),&v->overlay);if(error)throw std::runtime_error("Overlay creation failed: "+std::to_string(error)); - overlays->SetOverlayInputMethod(v->overlay,key=="notifications"?vr::VROverlayInputMethod_None:vr::VROverlayInputMethod_Mouse);vr::HmdVector2_t scale{{float(width),float(height)}};overlays->SetOverlayMouseScale(v->overlay,&scale);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_HideLaserIntersection,false);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_SendVRSmoothScrollEvents,true);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_VisibleInDashboard,true);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible,true);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_SortWithNonSceneOverlays,false);overlays->SetOverlaySortOrder(v->overlay,key=="notifications"?100:0); + overlays->SetOverlayInputMethod(v->overlay,key=="notifications"?vr::VROverlayInputMethod_None:vr::VROverlayInputMethod_Mouse);vr::HmdVector2_t scale{{float(width),float(height)}};overlays->SetOverlayMouseScale(v->overlay,&scale);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_HideLaserIntersection,false);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_SendVRSmoothScrollEvents,true);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_VisibleInDashboard,true);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible,true);overlays->SetOverlayFlag(v->overlay,vr::VROverlayFlags_SortWithNonSceneOverlays,false);overlays->SetOverlaySortOrder(v->overlay,0); // Keep the runtime's full dashboard controls, including its keyboard and close buttons. if(dock){ @@ -177,9 +215,9 @@ static void plugin_thumbnail(View& view,const std::string& encoded){ for(int y=0;ySetOverlayRaw(view.thumbnail,pixels.data(),128,128,4); } -static void commands(){std::deque commands;{std::lock_guard lock(queue_mutex);commands.swap(queue);}for(const auto& c:commands){try{auto kind=c.value("kind","");if(kind=="haptic"){auto key=c.value("view","");if(views.count(key)&&views.at(key)->shown&&!views.at(key)->closing)hover_haptic(views.at(key)->overlay,views.at(key)->haptic_at);}else if(kind=="manager.open"){if(views.count("framely.manager")&&views.at("framely.manager")->closing){std::lock_guard lock(queue_mutex);queue.push_back(c);continue;}auto page=c.value("page",std::string{});auto& manager=create_view("framely.manager","Framely 插件管理",origin+"/manager"+(page=="catalog"?"#catalog":""),true,1440,810);if(page=="catalog"){auto* frame=manager.browser->get_main_frame(manager.browser);execute(frame,"location.hash=\"catalog\";window.dispatchEvent(new Event(\"hashchange\"))");frame->base.release(&frame->base);}menu_open=false;}else if(kind=="agreement.decline"){auto key=c.value("view","");if(key=="menu"){menu_open=false;if(views.count("menu")){show(*views.at("menu"),false);views.at("menu")->browser->reload(views.at("menu")->browser);}}else if(views.count(key))close_view(*views.at(key));}else if(kind=="manager.close"){if(views.count("framely.manager"))close_view(*views.at("framely.manager"));}else if(kind=="menu.close"){menu_open=false;}else if(kind=="window.open"){std::string plugin=c.at("plugin"),win=c.at("window");auto key="framely.window."+plugin+"."+win;if(views.count(key)&&views.at(key)->closing){std::lock_guard lock(queue_mutex);queue.push_back(c);continue;}auto spec=c.at("spec");auto& opened=create_view("framely.window."+plugin+"."+win,spec.at("title"),origin+"/window/"+plugin+"/"+win,spec.value("dockIcon",false),spec.value("width",1600),spec.value("height",900),plugin,spec.value("widthMeters",3.f));plugin_thumbnail(opened,spec.value("iconData",std::string{}));menu_open=false;}else if(kind=="window.close"){auto key="framely.window."+c.at("plugin").get()+"."+c.at("window").get();if(views.count(key))close_view(*views.at(key));}else if(kind=="plugin.disabled"){for(auto&[key,v]:views)if(v->plugin==c.at("plugin"))close_view(*v);}else if(kind=="notification.changed"){auto& v=create_view("notifications","Framely notifications",origin+"/notifications",false,600,360);anchor_to_head(v,.42f,.42f,.32f,-.8f);v.host->was_hidden(v.host,0);v.host->invalidate(v.host,PET_VIEW);}else if(kind=="keyboard"){auto key=c.value("view","menu");if(!views.count(key))continue;auto& v=*views.at(key);if(v.closing)continue;close_keyboard();v.host->set_focus(v.host,1);keyboard_frame=v.browser->get_focused_frame(v.browser);keyboard_view=key;keyboard_target=v.overlay;++keyboard_token;auto existing=c.value("existing","");if(key=="menu")overlays->SetOverlayFlag(v.overlay,vr::VROverlayFlags_WantsModalBehavior,false);auto e=overlays->ShowKeyboardForOverlay(v.overlay,c.value("password",false)?vr::k_EGamepadTextInputModePassword:vr::k_EGamepadTextInputModeNormal,c.value("multiline",false)?vr::k_EGamepadTextInputLineModeMultipleLines:vr::k_EGamepadTextInputLineModeSingleLine,vr::KeyboardFlag_Modal|vr::KeyboardFlag_Minimal|vr::KeyboardFlag_ShowArrowKeys,"Framely",4096,existing.c_str(),keyboard_token);keyboard_open=e==vr::VROverlayError_None;keyboard_menu.begin(keyboard_open&&key=="menu");if(!keyboard_open)close_keyboard();}else if(kind=="notifications.empty"){if(views.count("notifications"))show(*views.at("notifications"),false);}}catch(const std::exception& e){std::cerr<<"Native command error: "< commands;{std::lock_guard lock(queue_mutex);commands.swap(queue);}for(const auto& c:commands){try{auto kind=c.value("kind","");if(kind=="haptic"){auto key=c.value("view","");if(views.count(key)&&views.at(key)->shown&&!views.at(key)->closing)hover_haptic(views.at(key)->overlay,views.at(key)->haptic_at);}else if(kind=="manager.open"){if(views.count("framely.manager")&&views.at("framely.manager")->closing){std::lock_guard lock(queue_mutex);queue.push_back(c);continue;}auto page=c.value("page",std::string{});auto& manager=create_view("framely.manager","Framely 插件管理",origin+"/manager"+((page=="catalog"||page=="updates"||page=="notification-settings")?"#"+page:""),true,1440,810);if(page=="catalog"||page=="updates"||page=="notification-settings"){auto* frame=manager.browser->get_main_frame(manager.browser);execute(frame,"location.hash="+json(page).dump()+";window.dispatchEvent(new Event(\"hashchange\"))");frame->base.release(&frame->base);}menu_open=false;}else if(kind=="agreement.decline"){auto key=c.value("view","");if(key=="menu"){menu_open=false;if(views.count("menu")){show(*views.at("menu"),false);views.at("menu")->browser->reload(views.at("menu")->browser);}}else if(views.count(key))close_view(*views.at(key));}else if(kind=="manager.close"){if(views.count("framely.manager"))close_view(*views.at("framely.manager"));}else if(kind=="menu.close"){menu_open=false;}else if(kind=="window.open"){std::string plugin=c.at("plugin"),win=c.at("window");auto key="framely.window."+plugin+"."+win;if(views.count(key)&&views.at(key)->closing){std::lock_guard lock(queue_mutex);queue.push_back(c);continue;}auto spec=c.at("spec");auto& opened=create_view("framely.window."+plugin+"."+win,spec.at("title"),origin+"/window/"+plugin+"/"+win,spec.value("dockIcon",false),spec.value("width",1600),spec.value("height",900),plugin,spec.value("widthMeters",3.f));plugin_thumbnail(opened,spec.value("iconData",std::string{}));menu_open=false;}else if(kind=="window.close"){auto key="framely.window."+c.at("plugin").get()+"."+c.at("window").get();if(views.count(key))close_view(*views.at(key));}else if(kind=="plugin.disabled"){for(auto&[key,v]:views)if(v->plugin==c.at("plugin"))close_view(*v);}else if(kind=="notification.changed"){auto& v=create_view("notifications","Framely notifications",origin+"/notifications",false,600,360);anchor_notification(v);v.host->was_hidden(v.host,0);v.host->invalidate(v.host,PET_VIEW);}else if(kind=="keyboard"){auto key=c.value("view","menu");if(!views.count(key))continue;auto& v=*views.at(key);if(v.closing)continue;close_keyboard();v.host->set_focus(v.host,1);keyboard_frame=v.browser->get_focused_frame(v.browser);keyboard_view=key;keyboard_target=v.overlay;++keyboard_token;auto existing=c.value("existing","");if(key=="menu")overlays->SetOverlayFlag(v.overlay,vr::VROverlayFlags_WantsModalBehavior,false);auto e=overlays->ShowKeyboardForOverlay(v.overlay,c.value("password",false)?vr::k_EGamepadTextInputModePassword:vr::k_EGamepadTextInputModeNormal,c.value("multiline",false)?vr::k_EGamepadTextInputLineModeMultipleLines:vr::k_EGamepadTextInputLineModeSingleLine,vr::KeyboardFlag_Modal|vr::KeyboardFlag_Minimal|vr::KeyboardFlag_ShowArrowKeys,"Framely",4096,existing.c_str(),keyboard_token);keyboard_open=e==vr::VROverlayError_None;keyboard_menu.begin(keyboard_open&&key=="menu");if(!keyboard_open)close_keyboard();}else if(kind=="notifications.empty"){if(views.count("notifications")){views.at("notifications")->notification_content=false;show(*views.at("notifications"),false);}}}catch(const std::exception& e){std::cerr<<"Native command error: "<get_main_frame(v.browser);execute(frame,"window.dispatchEvent(new Event('framely.back'))");frame->base.release(&frame->base);}break;case vr::VREvent_FocusLeave:v.host->send_mouse_move_event(v.host,&mouse,1);break;case vr::VREvent_MouseMove:v.host->send_mouse_move_event(v.host,&mouse,0);break;case vr::VREvent_MouseButtonDown:case vr::VREvent_MouseButtonUp:{v.host->set_focus(v.host,1);auto button=e.data.mouse.button==vr::VRMouseButton_Right?MBT_RIGHT:MBT_LEFT;mouse.modifiers=e.eventType==vr::VREvent_MouseButtonDown?EVENTFLAG_LEFT_MOUSE_BUTTON:0;v.host->send_mouse_click_event(v.host,&mouse,button,e.eventType==vr::VREvent_MouseButtonUp,1);break;}case vr::VREvent_ScrollSmooth:case vr::VREvent_ScrollDiscrete:mouse.x=v.width/2;mouse.y=v.height/2;v.host->send_mouse_wheel_event(v.host,&mouse,int(e.data.scroll.xdelta*120),int(e.data.scroll.ydelta*120));break;case vr::VREvent_OverlayGamepadFocusLost:if(!keyboard_open&&!v.dashboard&&v.key!="menu"&&v.key!="notifications")close_view(v);break;default:break;}} +static void overlay_input(View& v,const vr::VREvent_t& e){if(v.closing||(v.key=="notifications"&&!v.shown))return;if(keyboard_event(e,v.overlay))return;laser_cursor(v.overlay,e);cef_mouse_event_t mouse{};mouse.x=int(e.data.mouse.x);mouse.y=v.height-int(e.data.mouse.y);switch(e.eventType){case vr::VREvent_Modal_Cancel:if(v.key=="menu"&&!keyboard_open&&!keyboard_menu.holds(std::chrono::steady_clock::now())&&menu_button.can_cancel(std::chrono::steady_clock::now())){menu_open=false;std::cout<<"Menu closed: outside click"<get_main_frame(v.browser);execute(frame,"window.dispatchEvent(new Event('framely.back'))");frame->base.release(&frame->base);}break;case vr::VREvent_FocusLeave:if(v.key!="notifications"||!v.notification_click.active)v.host->send_mouse_move_event(v.host,&mouse,1);break;case vr::VREvent_MouseMove:if(v.key!="notifications"||!v.notification_click.hold(mouse.x,mouse.y,std::chrono::steady_clock::now()))v.host->send_mouse_move_event(v.host,&mouse,0);break;case vr::VREvent_MouseButtonDown:case vr::VREvent_MouseButtonUp:{v.host->set_focus(v.host,1);auto button=e.data.mouse.button==vr::VRMouseButton_Right?MBT_RIGHT:MBT_LEFT;if(v.key=="notifications"&&button==MBT_LEFT){auto now=std::chrono::steady_clock::now();if(e.eventType==vr::VREvent_MouseButtonDown)v.notification_click.begin(mouse.x,mouse.y,now);else v.notification_click.release(mouse.x,mouse.y,now);}mouse.modifiers=e.eventType==vr::VREvent_MouseButtonDown?EVENTFLAG_LEFT_MOUSE_BUTTON:0;v.host->send_mouse_click_event(v.host,&mouse,button,e.eventType==vr::VREvent_MouseButtonUp,1);break;}case vr::VREvent_ScrollSmooth:case vr::VREvent_ScrollDiscrete:mouse.x=v.width/2;mouse.y=v.height/2;v.host->send_mouse_wheel_event(v.host,&mouse,int(e.data.scroll.xdelta*120),int(e.data.scroll.ydelta*120));break;case vr::VREvent_OverlayGamepadFocusLost:if(!keyboard_open&&!v.dashboard&&v.key!="menu"&&v.key!="notifications")close_view(v);break;default:break;}} int main(int argc,char** argv){ std::cout.setf(std::ios::unitbuf);std::string initial;int port=0;for(int i=1;i+1closed=true;v->host->base.release(&v->host->base);v->host=nullptr;v->browser->base.release(&v->browser->base);v->browser=nullptr;}}; display_handler.api.on_console_message=[](cef_display_handler_t*,cef_browser_t* b,cef_log_severity_t,const cef_string_t* message,const cef_string_t*,int){auto* v=find_browser(b);std::cout<<"UI ["<<(v?v->key:"loading")<<"] "<get_url(r);auto s=text(uri);cef_string_userfree_utf16_free(uri);return int(s.rfind(origin+"/",0)!=0&&s!="about:blank");}; + request_handler.api.on_before_browse=[](cef_request_handler_t*,cef_browser_t* b,cef_frame_t* f,cef_request_t* r,int,int){auto* uri=r->get_url(r);auto s=text(uri);cef_string_userfree_utf16_free(uri);auto* v=find_browser(b);return int(!allow_plugin_navigation(s,origin,!f||f->is_main(f),v&&!v->plugin.empty()));}; request_handler.api.on_render_process_terminated=[](cef_request_handler_t*,cef_browser_t* b,cef_termination_status_t,int,const cef_string_t*){auto* v=find_browser(b);if(v){std::cerr<<"Renderer crashed: "<key<<"\n";close_view(*v);if(v->key=="menu")running=0;}}; client.api.get_render_handler=[](cef_client_t*){return render_handler.acquire();};client.api.get_life_span_handler=[](cef_client_t*){return life_handler.acquire();};client.api.get_display_handler=[](cef_client_t*){return display_handler.acquire();};client.api.get_request_handler=[](cef_client_t*){return request_handler.acquire();}; vr::VROverlayHandle_t button=0;auto err=overlays->CreateOverlay("framely.dock.button","Framely",&button);if(err){cef_shutdown();vr::VR_Shutdown();return 6;}auto pixels=dock_icon(false,false);overlays->SetOverlayRaw(button,pixels.data(),dock_canvas_width,dock_canvas_height,4);overlays->SetOverlayInputMethod(button,vr::VROverlayInputMethod_Mouse);vr::HmdVector2_t scale{{dock_canvas_width,dock_canvas_height}};overlays->SetOverlayMouseScale(button,&scale);vr::VROverlayIntersectionMaskPrimitive_t mask{};mask.m_nPrimitiveType=vr::OverlayIntersectionPrimitiveType_Rectangle;mask.m_Primitive.m_Rectangle={dock_icon_left,0,128,128};if(overlays->SetOverlayIntersectionMask(button,&mask,1)){std::cerr<<"Dock input mask unavailable\n";overlays->DestroyOverlay(button);cef_shutdown();vr::VR_Shutdown();return 6;}overlays->SetOverlayFlag(button,vr::VROverlayFlags_VisibleInDashboard,true);overlays->SetOverlayFlag(button,vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible,true);overlays->SetOverlayFlag(button,vr::VROverlayFlags_SortWithNonSceneOverlays,false);overlays->SetOverlaySortOrder(button,0);overlays->SetOverlayFlag(button,vr::VROverlayFlags_HideLaserIntersection,false); try{create_view("menu","Framely",initial,false,600,840);}catch(const std::exception& e){std::cerr< lock(queue_mutex);for(const auto& c:data.at("commands")){if(queue.size()<256)queue.push_back(c);}failures=0;}catch(const std::exception& e){if(++failures>=10){std::cerr<<"Agent connection lost: "< lock(queue_mutex);for(const auto& c:data.at("commands")){if(queue.size()<256)queue.push_back(c);}failures=0;}catch(const std::exception& e){if(++failures>=10){std::cerr<<"Agent connection lost: "<PollNextEvent(&global,sizeof(global))){if(global.eventType==vr::VREvent_Quit){vr_system->AcknowledgeQuit_Exiting();running=0;}keyboard_event(global);}vr::VROverlayHandle_t dock=0;vr::HmdVector2_t mouse_scale{};bool visible=!overlays->FindOverlay("valve.steam.gamepadui.bar",&dock)&&overlays->IsDashboardVisible()&&overlays->IsOverlayVisible(dock)&&!overlays->GetOverlayMouseScale(dock,&mouse_scale); if(visible&&(!was_visible||dock!=last_dock)){uv={};anchored_at={};sampled_at={};}last_dock=dock; // Width and aspect are only ray-solver seeds; physical size comes from intersections. @@ -218,7 +256,7 @@ int main(int argc,char** argv){ vr::HmdMatrix34_t frame{};bool frame_ok=visible&&fresh&&!overlays->GetTransformForOverlayCoordinates(dock,vr::TrackingUniverseStanding,{{mouse_scale.v[0]*.5f,mouse_scale.v[1]*.5f}},¢er)&&dock_rigid_frame(center,frame); auto sample_now=std::chrono::steady_clock::now(); if(frame_ok&&sample_now-sampled_at>=std::chrono::milliseconds(100)){sampled_at=sample_now;anchor_ok=dock_probe_origin(center,head)&&dock_uv_anchor(overlays,dock,center,head,width,height,anchor,evidence,size,uv);} - auto now=std::chrono::steady_clock::now();if(anchor_ok){saved_anchor=anchor;saved_frame=frame;saved_size=size;saved_evidence=evidence;anchored_at=now;}else if(frame_ok&&now-anchored_at0)notification_curve_radius=evidence.radius;saved_anchor=anchor;saved_frame=frame;saved_size=size;saved_evidence=evidence;anchored_at=now;}else if(frame_ok&&now-anchored_atIsOverlayVisible(button))overlays->ShowOverlay(button);if(views.count("menu")){MenuPlacement popup{};auto& v=*views.at("menu");vr::HmdMatrix34_t main_pose{};vr::VROverlayHandle_t main_overlay=0;vr::HmdVector2_t main_scale{};const vr::HmdMatrix34_t* main=nullptr; for(auto&[key,view]:views)if(view->dashboard&&view->shown&&!view->closing)main_overlay=view->overlay; @@ -269,10 +307,20 @@ int main(int argc,char** argv){ auto error=overlays->SetOverlayIntersectionMask(v->overlay,&mask,1); if(error){std::cerr<<"Notification input mask failed "<notification_content=visible; } } } + if(key=="notifications"){ + if(v->notification_click.active&&std::chrono::steady_clock::now()-v->notification_click.pressed>std::chrono::milliseconds(800)){ + // A release outside the overlay may not arrive. Never leave CEF pressed. + cef_mouse_event_t outside{};outside.x=outside.y=-1; + v->host->send_mouse_click_event(v->host,&outside,MBT_LEFT,1,1);v->notification_click.clear(); + } + if(v->notification_content&&!v->placement.valid)anchor_notification(*v); + if(v->placement.valid)v->placement.curve(overlays,v->overlay,v->placement.width,notification_curve_radius); + show(*v,v->notification_content&&v->placement.valid); + } } {json snapshot=json::object();for(auto&[key,v]:views){if(key=="menu"||key=="framely.manager"||key.rfind("framely.window.",0)==0)snapshot[key]=capture_view_visible(v->closing,overlays->IsOverlayVisible(v->overlay),v->dashboard,overlays->IsDashboardVisible(),overlays->IsActiveDashboardOverlay(v->overlay));}std::lock_guard lock(visibility_mutex);visible_views=std::move(snapshot);} if(keyboard_close_pending){ @@ -282,7 +330,7 @@ int main(int argc,char** argv){ close_keyboard(false); } for(auto i=views.begin();i!=views.end();){auto& v=*i->second;if(v.closed){overlays->ClearOverlayTexture(v.overlay);overlays->DestroyOverlay(v.overlay);if(v.thumbnail)overlays->DestroyOverlay(v.thumbnail);glDeleteTextures(2,v.textures);i=views.erase(i);}else ++i;} - bool hovered=anchor_ok&&overlays->IsHoverTargetOverlay(button);if(hovered&&!icon_hover)hover_haptic(button,button_haptic_at);if(icon_active!=menu_open||icon_hover!=hovered){icon_active=menu_open;pixels=dock_icon(hovered,menu_open);overlays->SetOverlayRaw(button,pixels.data(),dock_canvas_width,dock_canvas_height,4);}icon_hover=hovered; + bool hovered=anchor_ok&&overlays->IsHoverTargetOverlay(button);if(hovered&&!icon_hover)hover_haptic(button,button_haptic_at);const auto badge=notification_badge_count.load();if(icon_active!=menu_open||icon_hover!=hovered||icon_badge!=badge){icon_badge=badge;icon_active=menu_open;pixels=dock_icon(hovered,menu_open);overlays->SetOverlayRaw(button,pixels.data(),dock_canvas_width,dock_canvas_height,4);}icon_hover=hovered; auto sync=overlays->WaitFrameSync(8); // A timeout already waited: never append another sleep to it. Bound retries // only for immediate API failures, so a broken sync call cannot busy-spin. diff --git a/native/notification_badge.h b/native/notification_badge.h new file mode 100644 index 0000000..4e04f93 --- /dev/null +++ b/native/notification_badge.h @@ -0,0 +1,28 @@ +#pragma once +#include +#include +#include +#include + +// Rasterize the unread count directly onto the Dock's 128px icon texture. +// The zero state leaves the original icon byte-for-byte unchanged. +inline void notification_badge(std::vector& rgba, unsigned count) { + if (!count || rgba.size()!=128*128*4) return; + const std::string label=count>99?"99+":std::to_string(count); + const int text_width=int(label.size())*12-3,width=text_width+14,left=124-width,top=4,height=29; + for(int y=top;y36)continue; + auto i=(y*128+x)*4;rgba[i]=147;rgba[i+1]=197;rgba[i+2]=237;rgba[i+3]=255; + } + // Compact 3x5 numerals retain a readable count at the Frame Dock's display scale. + constexpr uint16_t digits[]={0b111101101101111,0b010110010010111,0b111001111100111,0b111001111001111,0b101101111001001,0b111100111001111,0b111100111101111,0b111001001001001,0b111101111101111,0b111101111001111}; + for(size_t n=0;n #include #include +#include +#include "geometry_math.h" + +struct NotificationPlacement { + vr::HmdMatrix34_t transform{}; + float width=.32f,radius=0; +}; + +// Keep the compact toast at the upper right, but turn its front toward the eye. +// Copying the headset rotation leaves a side-offset overlay facing past the user. +inline bool notification_placement(const vr::HmdMatrix34_t& headset,float radius,NotificationPlacement& out){ + vr::HmdMatrix34_t head{}; + if(!dock_rigid_frame(headset,head)||!std::isfinite(radius)||radius<=0)return false; + vr::HmdVector3_t normal{},head_up{{head.m[0][1],head.m[1][1],head.m[2][1]}}; + for(int r=0;r<3;r++){ + out.transform.m[r][3]=head.m[r][3]+head.m[r][0]*.24f+head.m[r][1]*.12f-head.m[r][2]*1.f; + normal.v[r]=head.m[r][3]-out.transform.m[r][3]; + } + if(!normalize_dock_vector(normal))return false; + auto right=cross_dock_vector(head_up,normal); + if(!normalize_dock_vector(right))return false; + auto up=cross_dock_vector(normal,right); + for(int r=0;r<3;r++){ + out.transform.m[r][0]=right.v[r];out.transform.m[r][1]=up.v[r];out.transform.m[r][2]=normal.v[r]; + } + out.radius=radius;return true; +} struct NotificationBounds { float x=0,y=0,width=0,height=0; bool visible() const {return width>0&&height>0;} }; -// The submitted BGRA buffer is flipped in on_paint to OpenVR's bottom-left origin. +// BGRA rows are bottom-up after on_paint. Intersection rectangles use top-left +// coordinates, so the alpha bounds must be flipped back before submitting the mask. inline NotificationBounds notification_bounds(const std::vector& pixels,int width,int height,int scale){ if(width<=0||height<=0||scale<=0||pixels.size()!=size_t(width)*height*4)return {}; int left=width,low=height,right=-1,high=-1; @@ -17,5 +45,22 @@ inline NotificationBounds notification_bounds(const std::vector& pixels if(xright)right=x;if(yhigh)high=y; } if(rightstd::chrono::milliseconds(800)||std::hypot(float(px-x),float(py-y))>18.f)cancelled=true; + return !cancelled; + } + bool release(int& px,int& py,Time now){ + bool stable=hold(px,py,now);if(stable){px=x;py=y;}active=false;return stable; + } + void clear(){active=false;cancelled=false;} +}; diff --git a/native/plugin_navigation.h b/native/plugin_navigation.h new file mode 100644 index 0000000..1260fa6 --- /dev/null +++ b/native/plugin_navigation.h @@ -0,0 +1,21 @@ +#pragma once +#include + +// Other localhost ports are only for embedded pages in plugin windows. +// The main frame remains on the Framely origin. +inline bool allow_plugin_navigation(const std::string& url, const std::string& origin, + bool main_frame, bool plugin_window) { + if (url == "about:blank" || url.rfind(origin + "/", 0) == 0) return true; + if (main_frame || !plugin_window) return false; + const std::string prefix = "http://localhost:"; + if (url.rfind(prefix, 0) != 0) return false; + auto end = url.find('/', prefix.size()); + if (end == std::string::npos || end == prefix.size()) return false; + unsigned port = 0; + for (auto i = prefix.size(); i < end; ++i) { + if (url[i] < '0' || url[i] > '9') return false; + port = port * 10 + unsigned(url[i] - '0'); + if (port > 65535) return false; + } + return port > 0; +} diff --git a/package-lock.json b/package-lock.json index eacbd8c..827cef8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "examples/showcase" ], "dependencies": { + "@tabler/icons-react": "3.48.0", "react": "19.2.0", "react-dom": "19.2.0" }, @@ -713,6 +714,32 @@ "node": ">= 10" } }, + "node_modules/@tabler/icons": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.48.0.tgz", + "integrity": "sha512-lQk06gVHNVBnJp0UOgtth54mcZBJM7rdYKsAxKOProj1bbSAx+/xiuOWfHlyvgdL3M+Y+I0HINrjHa3ygp8eXQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.48.0.tgz", + "integrity": "sha512-OCeAlpaWHEMV+EsgdVeGQ3YuKfmfnThZhc++CaZ/ujYzi5+/DONDQlX4OGvH1ikYhIfmc0grI4otJgHy7QAvng==", + "license": "MIT", + "dependencies": { + "@tabler/icons": "3.48.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@types/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", diff --git a/package.json b/package.json index 3df39c2..3aa3036 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "plugin:init": "node tools/plugin-dev.mjs init" }, "dependencies": { + "@tabler/icons-react": "3.48.0", "react": "19.2.0", "react-dom": "19.2.0" }, diff --git a/sdk/src/index.tsx b/sdk/src/index.tsx index 3aa482c..fd7b4f1 100644 --- a/sdk/src/index.tsx +++ b/sdk/src/index.tsx @@ -4,8 +4,8 @@ export * from './lifecycle'; import {installScrollbars} from './scrollbars'; import React from 'react'; import {createRoot} from 'react-dom/client'; -export interface NotificationAction {id:string;label:string;icon:string} -export interface Notification {id:string;title:string;body:string;image?:string;actions?:NotificationAction[];durationMs?:number} +export interface NotificationAction {id:string;label:string;icon?:string;closeOnClick?:boolean;removeFromInboxOnClick?:boolean} +export interface Notification {id:string;title:string;body:string;image?:string;actions?:NotificationAction[];durationMs?:number;inbox?:boolean} type Bridge={request:(op:string,p?:unknown)=>Promise;subscribe:(f:(e:unknown)=>void)=>()=>void}; function bridge():Bridge{const b=(window as any).__framelyBridge;if(!b)throw new Error('This page requires the Framely plugin host');return b;} export const framely={ @@ -14,7 +14,7 @@ export const framely={ call:(method:string,params:unknown={}):Promise=>bridge().request('call',{method,params}), windows:{open:(window:string)=>bridge().request('window.open',{window}),close:(window:string)=>bridge().request('window.close',{window})}, dependencies:():Promise=>bridge().request('dependencies'), - notifications:{send:(notification:Notification)=>bridge().request('notification.send',{notification}),remove:(id:string)=>bridge().request('notification.remove',{id})}, + notifications:{send:(notification:Notification)=>bridge().request('notification.send',{notification}),remove:(id:string)=>bridge().request('notification.remove',{id}),dismiss:(id:string)=>bridge().request('notification.dismiss',{id})}, onEvent:(callback:(event:unknown)=>void)=>bridge().subscribe(callback), }; export function registerPlugin(pages:{QuickPage:React.ComponentType;WindowPage?:React.ComponentType;windows?:Record}){installScrollbars();const key=location.pathname.split('/').pop();const Page=key==='quick'?pages.QuickPage:pages.windows?.[key??'']??pages.WindowPage??pages.QuickPage;const style=document.createElement('style');style.textContent='*{box-sizing:border-box}body{margin:0;padding:20px;background:#101820;color:#edf2fa;font:20px system-ui}button,input,textarea{font:inherit;color:inherit;border:1px solid #52627a;border-radius:6px;padding:12px;background:#24333e;margin:6px 6px 6px 0;min-height:48px}button{cursor:pointer}button:disabled{opacity:.5}input,textarea{width:100%}p{line-height:1.6}small{color:#acb9cf}';document.head.append(style);createRoot(document.getElementById('root')!).render();console.log('FRAMELY_PLUGIN_READY');} diff --git a/src/model.rs b/src/model.rs index 01f8d69..223466b 100644 --- a/src/model.rs +++ b/src/model.rs @@ -583,13 +583,121 @@ pub struct Database { pub update_source: Option, #[serde(default, skip_serializing_if = "UpdateChannel::is_stable")] pub update_channel: UpdateChannel, + #[serde(default)] + pub update_check: UpdateCheckSettings, + #[serde(default)] + pub update_notice: Option, + #[serde(default)] + pub notification_inbox: BTreeMap, + #[serde(default)] + pub notification_settings: NotificationSettings, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NotificationPreference { + #[serde(default = "yes")] + pub popups: bool, + #[serde(default = "yes")] + pub badge: bool, +} +impl Default for NotificationPreference { + fn default() -> Self { + Self { + popups: true, + badge: true, + } + } +} +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NotificationSettings { + #[serde(default = "yes")] + pub popups: bool, + #[serde(default = "yes")] + pub badge: bool, + #[serde(default)] + pub plugins: BTreeMap, +} +impl Default for NotificationSettings { + fn default() -> Self { + Self { + popups: true, + badge: true, + plugins: BTreeMap::new(), + } + } +} +impl NotificationSettings { + pub fn popups_for(&self, plugin: &str) -> bool { + self.popups && self.plugins.get(plugin).is_none_or(|p| p.popups) + } + pub fn badge_for(&self, plugin: &str) -> bool { + self.badge && self.plugins.get(plugin).is_none_or(|p| p.badge) + } +} +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NotificationSettingsPatch { + pub plugin: Option, + pub popups: Option, + pub badge: Option, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct UpdateCheckSettings { + pub enabled: bool, + pub interval_hours: u64, +} +impl Default for UpdateCheckSettings { + fn default() -> Self { + Self { + enabled: true, + interval_hours: 6, + } + } +} +impl UpdateCheckSettings { + pub fn validate(&self) -> Result<()> { + ensure!( + (1..=168).contains(&self.interval_hours), + "自动检查间隔须为 1-168 小时" + ); + Ok(()) + } +} +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct UpdateNotice { + pub source: UpdateSource, + pub channel: UpdateChannel, + pub version: String, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct NotificationEntry { + pub plugin: String, + pub plugin_name: String, + pub created_at: u64, + pub expires_at: Option, + pub toast: bool, + pub in_inbox: bool, + #[serde(default)] + pub read: bool, + pub notification: Notification, + #[serde(default)] + pub update: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Action { pub id: String, pub label: String, + #[serde(default)] pub icon: String, + #[serde(default = "yes")] + pub close_on_click: bool, + #[serde(default = "yes")] + pub remove_from_inbox_on_click: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -603,6 +711,8 @@ pub struct Notification { pub actions: Vec, #[serde(default = "ttl")] pub duration_ms: u64, + #[serde(default)] + pub inbox: bool, } fn ttl() -> u64 { 8000 @@ -615,7 +725,7 @@ impl Notification { "Notification limit exceeded" ); ensure!( - (1000..=60000).contains(&self.duration_ms), + self.duration_ms == 0 || (1000..=60000).contains(&self.duration_ms), "Invalid notification duration" ); let mut ids = std::collections::BTreeSet::new(); diff --git a/src/service.rs b/src/service.rs index 54d54ea..0f9e215 100644 --- a/src/service.rs +++ b/src/service.rs @@ -31,7 +31,12 @@ pub struct Service { pub manager: u32, running: BTreeMap, pub events: Events, - notifications: BTreeMap, + notifications: BTreeMap, + automatic_updater: crate::update::Updater, + auto_update_job: Option<(String, UpdateSource, UpdateChannel)>, + next_update_check: Instant, + last_update_check: Option, + auto_update_error: Option, rates: BTreeMap>, recovery: BTreeMap, updater: crate::update::Updater, @@ -43,6 +48,7 @@ pub struct Service { } impl Service { pub fn prepare_uninstall(&mut self) -> Result { + self.cancel_automatic_check(); self.updater.ensure_idle()?; self.updater.jobs.ensure_idle()?; let roots: Vec<_> = self.db.plugins.keys().cloned().collect(); @@ -73,8 +79,10 @@ impl Service { .with_context(|| format!("插件 {id} 卸载失败,本体保留,请重试"))?; fs::remove_dir_all(self.root.join("plugins").join(id))?; self.db.plugins.remove(id); + self.db.notification_settings.plugins.remove(id); self.runtime.remove(id); - self.save()?; + self.notifications.retain(|_, n| n.plugin != *id); + self.save_notifications()?; } Ok(json!({"uninstalled": ids})) } @@ -86,6 +94,7 @@ impl Service { } else { Database::default() }; + db.update_check.validate()?; if db.update_source.is_none() { db.update_source = fs::read(root.join("update-source.json")) .ok() @@ -146,6 +155,15 @@ impl Service { } } } + let notifications = db + .notification_inbox + .iter() + .map(|(key, entry)| { + let mut entry = entry.clone(); + entry.toast = false; + (key.clone(), entry) + }) + .collect(); let service = Self { verifier: Default::default(), root: root.into(), @@ -153,7 +171,12 @@ impl Service { manager, running: BTreeMap::new(), events: Arc::default(), - notifications: BTreeMap::new(), + notifications, + automatic_updater: Default::default(), + auto_update_job: None, + next_update_check: Instant::now() + Duration::from_secs(30), + last_update_check: None, + auto_update_error: None, rates: BTreeMap::new(), recovery: BTreeMap::new(), updater: crate::update::Updater::default(), @@ -222,7 +245,10 @@ impl Service { ); } fn cleanup_resources(&mut self, id: &str) { - self.notifications.retain(|_, v| v["plugin"] != id); + for entry in self.notifications.values_mut().filter(|v| v.plugin == id) { + entry.toast = false; + } + self.notifications.retain(|_, v| v.toast || v.in_inbox); self.rates.remove(id); process::event(&self.events, json!({"kind":"plugin.disabled","plugin":id})); process::event( @@ -507,6 +533,7 @@ impl Service { Ok(()) } pub fn shutdown(&mut self) { + self.cancel_automatic_check(); let roots: Vec<_> = self.running.keys().cloned().collect(); let ids = crate::relations::order(&self.db, &roots, false).unwrap_or(roots); for id in ids.into_iter().rev() { @@ -583,6 +610,7 @@ impl Service { } } pub fn maintenance(&mut self) { + self.expire_notifications(); if self.visibility.expire(Instant::now()) { self.publish_visibility(); } @@ -590,6 +618,7 @@ impl Service { if !self.agreement_accepted() { return; } + self.automatic_update_check(); let now = Instant::now(); let exited: Vec<_> = self .running @@ -690,6 +719,7 @@ impl Service { "status" | "events" | "host.ui.visibility" + | "host.notification.badge" | "ui.visibility.get" | "language.list" | "language.save" @@ -778,6 +808,9 @@ impl Service { self.stop_one(&id, "agreement-revoked"); } self.notifications.clear(); + self.db.notification_inbox.clear(); + self.cancel_automatic_check(); + self.save()?; process::event(&self.events, json!({"kind":"agreement.revoked"})); Ok(json!(true)) } @@ -805,7 +838,7 @@ impl Service { "status" => { self.maintenance(); Ok( - json!({"agreement":{"version":AGREEMENT_VERSION,"accepted":self.agreement_accepted()},"version":env!("CARGO_PKG_VERSION"),"build":fs::read_to_string(self.root.join("current/VERSION")).unwrap_or_else(|_|env!("CARGO_PKG_VERSION").into()).trim(),"systemUpdate":fs::read(self.root.join("update-status.json")).ok().and_then(|b|serde_json::from_slice::(&b).ok()),"previousRelease":fs::read_to_string(self.root.join("previous-release")).ok().filter(|s|!s.trim().is_empty()),"apiVersion":API_VERSION,"runtime":self.runtime,"database":self.db,"running":self.running.iter_mut().filter_map(|(id,r)|if !r.exited(){Some(id.clone())}else{None}).collect::>(),"notifications":self.notifications.values().collect::>() }), + json!({"agreement":{"version":AGREEMENT_VERSION,"accepted":self.agreement_accepted()},"version":env!("CARGO_PKG_VERSION"),"build":fs::read_to_string(self.root.join("current/VERSION")).unwrap_or_else(|_|env!("CARGO_PKG_VERSION").into()).trim(),"systemUpdate":fs::read(self.root.join("update-status.json")).ok().and_then(|b|serde_json::from_slice::(&b).ok()),"previousRelease":fs::read_to_string(self.root.join("previous-release")).ok().filter(|s|!s.trim().is_empty()),"apiVersion":API_VERSION,"runtime":self.runtime,"database":self.db,"running":self.running.iter_mut().filter_map(|(id,r)|if !r.exited(){Some(id.clone())}else{None}).collect::>(),"notifications":self.notifications.values().filter(|n| n.toast).collect::>(),"inbox":self.notifications.values().filter(|n| n.in_inbox).collect::>(),"notificationBadge":self.notification_badge(),"automaticUpdate":{"checking":self.auto_update_job.is_some(),"lastCheckedAt":self.last_update_check,"error":self.auto_update_error} }), ) } "inspect" => { @@ -933,7 +966,9 @@ impl Service { } } self.db.plugins.remove(&id); - self.save()?; + self.db.notification_settings.plugins.remove(&id); + self.notifications.retain(|_, n| n.plugin != id); + self.save_notifications()?; fs::remove_dir_all(self.root.join("plugins").join(&id))?; if p["purge"].as_bool() == Some(true) { let data = self.root.join("data").join(&id); @@ -997,42 +1032,103 @@ impl Service { ); Ok(json!(true)) } - "notification.send" => self.notify(&id, p["notification"].clone()), - "notification.remove" => { - self.active(&id)?; - let key = p["id"].as_str().context("Missing notification ID")?; - valid_id(key)?; - self.notifications.remove(&format!("{id}:{key}")); - process::event(&self.events, json!({"kind":"notification.changed"})); + "host.notification.badge" => Ok(json!(self.notification_badge())), + "notification.settings.save" => { + let patch: NotificationSettingsPatch = serde_json::from_value(p)?; + ensure!( + patch.popups.is_some() || patch.badge.is_some(), + "Missing notification setting" + ); + let mut settings = self.db.notification_settings.clone(); + if let Some(id) = patch.plugin { + // Core notifications already use the empty sender identity. + if !id.is_empty() { + self.plugin(&id)?; + } + let preference = settings.plugins.entry(id).or_default(); + if let Some(value) = patch.popups { + preference.popups = value; + } + if let Some(value) = patch.badge { + preference.badge = value; + } + } else { + if let Some(value) = patch.popups { + settings.popups = value; + } + if let Some(value) = patch.badge { + settings.badge = value; + } + } + self.expire_notifications(); + let new_inbox = self + .notifications + .values() + .filter(|n| n.in_inbox || n.toast && !settings.popups_for(&n.plugin)) + .count(); + ensure!(new_inbox <= 256, "Notification inbox full"); + self.db.notification_settings = settings; + for entry in self.notifications.values_mut() { + if entry.toast && !self.db.notification_settings.popups_for(&entry.plugin) { + entry.toast = false; + entry.in_inbox = true; + } + } + self.save_notifications()?; Ok(json!(true)) } - "notification.action" => { - let key = p["id"].as_str().context("Missing ID")?; - let action = p["action"].as_str().context("Missing action")?; - let n = self - .notifications - .get(&format!("{id}:{key}")) - .context("Notification expired")?; - ensure!( - n["expiresAt"].as_u64().unwrap_or(0) > now_ms(), - "Notification expired" - ); - ensure!( - n["notification"]["actions"] - .as_array() - .is_some_and(|a| a.iter().any(|v| v["id"] == action)), - "Action not declared" - ); - process::event( - &self.events, - json!({"kind":"plugin.event","plugin":id,"event":"notification.action","data":{"id":key,"action":action}}), - ); - if self.active(&id)?.manifest.backend.is_some() { - self.handle("plugin.call",json!({"plugin":id,"method":"notification.action","params":{"id":key,"action":action}})) - } else { - Ok(json!(true)) + "notification.read" => { + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase", deny_unknown_fields)] + struct ReadEntry { + plugin: String, + id: String, + created_at: u64, } + let entries: Vec = serde_json::from_value(p["entries"].clone())?; + ensure!(entries.len() <= 256, "Too many notifications"); + let mut changed = false; + for item in entries { + if let Some(n) = self + .notifications + .get_mut(&format!("{}:{}", item.plugin, item.id)) + { + if n.in_inbox && !n.read && n.created_at == item.created_at { + n.read = true; + changed = true; + } + } + } + if changed { + self.save_notifications()?; + } + Ok(json!(true)) } + "notification.send" => self.notify(&id, p["notification"].clone()), + "notification.remove" | "notification.dismiss" => { + if !id.is_empty() { + self.plugin(&id)?; + } + let key = p["id"].as_str().context("Missing notification ID")?; + valid_id(key)?; + let compound = format!("{id}:{key}"); + if let Some(entry) = self.notifications.get_mut(&compound) { + ensure!( + p["createdAt"] + .as_u64() + .is_none_or(|time| time == entry.created_at), + "Notification has changed" + ); + entry.toast = false; + if method == "notification.remove" { + entry.in_inbox = false; + } + } + self.notifications.retain(|_, n| n.toast || n.in_inbox); + self.save_notifications()?; + Ok(json!(true)) + } + "notification.action" => self.notification_action(&id, &p), "events" => { let events: Vec<_> = self.events.lock().unwrap().drain(..).collect(); let mut out = Vec::new(); @@ -1055,8 +1151,7 @@ impl Service { out.push(e); } } - self.notifications - .retain(|_, v| v["expiresAt"].as_u64().unwrap_or(0) > now_ms()); + self.expire_notifications(); Ok(json!(out)) } "system.source.save" => { @@ -1072,6 +1167,7 @@ impl Service { } else { let _ = fs::remove_file(self.root.join("update-source.json")); } + self.reset_automatic_updates(); self.db.update_source = source; self.updater.clear(); self.save()?; @@ -1080,11 +1176,21 @@ impl Service { "system.channel.save" => { let channel: UpdateChannel = serde_json::from_value(p["channel"].clone())?; self.updater.ensure_idle()?; + self.reset_automatic_updates(); self.db.update_channel = channel; self.updater.clear(); self.save()?; Ok(json!(true)) } + "system.check.settings.save" => { + let settings: UpdateCheckSettings = serde_json::from_value(p["settings"].clone())?; + settings.validate()?; + self.cancel_automatic_check(); + self.db.update_check = settings; + self.next_update_check = Instant::now() + Duration::from_secs(30); + self.save()?; + Ok(json!(true)) + } "system.check.start" => self.updater.check( self.db.update_source.clone().context("请先设置更新源")?, self.db.update_channel, @@ -1305,15 +1411,269 @@ impl Service { } ensure!(rate.len() < 10, "Notification rate exceeded"); rate.push_back(now); + self.insert_notification(id, &name, n, None)?; + Ok(json!(true)) + } + fn insert_notification( + &mut self, + id: &str, + name: &str, + notification: Notification, + update: Option, + ) -> Result<()> { + self.expire_notifications(); + let key = format!("{id}:{}", notification.id); + let toast = self.db.notification_settings.popups_for(id); + let in_inbox = notification.inbox || !toast; ensure!( - self.notifications.len() < 64 - || self.notifications.contains_key(&format!("{id}:{}", n.id)), + !toast + || self.notifications.values().filter(|n| n.toast).count() < 64 + || self.notifications.get(&key).is_some_and(|n| n.toast), "Notification queue full" ); - let entry = json!({"plugin":id,"pluginName":name,"expiresAt":now_ms()+n.duration_ms,"notification":n}); - self.notifications.insert(format!("{id}:{}", n.id), entry); + ensure!( + !in_inbox + || self.notifications.values().filter(|n| n.in_inbox).count() < 256 + || self.notifications.get(&key).is_some_and(|n| n.in_inbox), + "Notification inbox full" + ); + let created_at = now_ms().max(self.notifications.get(&key).map_or(0, |n| n.created_at + 1)); + let expires_at = + (notification.duration_ms != 0).then(|| created_at + notification.duration_ms); + let entry = NotificationEntry { + plugin: id.into(), + plugin_name: name.into(), + created_at, + expires_at, + toast, + in_inbox, + read: false, + notification, + update, + }; + self.notifications.insert(key, entry); + self.save_notifications() + } + fn notification_badge(&self) -> usize { + if !self.agreement_accepted() { + return 0; + } + self.notifications + .values() + .filter(|n| n.in_inbox && !n.read && self.db.notification_settings.badge_for(&n.plugin)) + .count() + } + fn save_notifications(&mut self) -> Result<()> { + self.db.notification_inbox = self + .notifications + .iter() + .filter(|(_, n)| n.in_inbox) + .map(|(k, n)| (k.clone(), n.clone())) + .collect(); + self.save()?; process::event(&self.events, json!({"kind":"notification.changed"})); - Ok(json!(true)) + Ok(()) + } + fn expire_notifications(&mut self) { + let now = now_ms(); + let mut changed = false; + for n in self.notifications.values_mut() { + if n.toast && n.expires_at.is_some_and(|time| time <= now) { + n.toast = false; + changed = true; + } + } + self.notifications.retain(|_, n| n.toast || n.in_inbox); + if changed { + process::event(&self.events, json!({"kind":"notification.changed"})); + } + } + fn notification_action(&mut self, id: &str, p: &Value) -> Result { + self.expire_notifications(); + let key = p["id"].as_str().context("Missing ID")?; + let action = p["action"].as_str().context("Missing action")?; + let compound = format!("{id}:{key}"); + let entry = self + .notifications + .get(&compound) + .context("Notification expired")? + .clone(); + ensure!( + p["createdAt"] + .as_u64() + .is_none_or(|time| time == entry.created_at), + "Notification has changed" + ); + let policy = entry + .notification + .actions + .iter() + .find(|a| a.id == action) + .context("Action not declared")?; + let result = if let Some(update) = &entry.update { + ensure!( + self.db.update_source.as_ref() == Some(&update.source) + && self.db.update_channel == update.channel, + "Update source or channel has changed" + ); + match action { + "ignore" => json!(true), + "open" => { + process::event( + &self.events, + json!({"kind":"manager.open","page":"updates"}), + ); + json!({"openUpdates":true}) + } + _ => bail!("Action not declared"), + } + } else { + self.active(id)?; + let result = if self.plugin(id)?.manifest.backend.is_some() { + match self.handle("plugin.call", json!({"plugin":id,"method":"notification.action","params":{"id":key,"action":action}})) { + Ok(value) => value, + Err(error) => { + // Starting a failed backend may clear transient resources. Keep the + // message for retry, and never remove it as a successful action. + if self.notifications.get(&compound).is_none_or(|n| n.created_at == entry.created_at) { + self.notifications.insert(compound.clone(), entry.clone()); + self.save_notifications()?; + } + return Err(error); + } + } + } else { + json!(true) + }; + process::event( + &self.events, + json!({"kind":"plugin.event","plugin":id,"event":"notification.action","data":{"id":key,"action":action}}), + ); + result + }; + if let Some(n) = self + .notifications + .get_mut(&compound) + .filter(|n| n.created_at == entry.created_at) + { + if policy.close_on_click { + n.toast = false; + } + if policy.remove_from_inbox_on_click { + n.in_inbox = false; + } + } + self.notifications.retain(|_, n| n.toast || n.in_inbox); + self.save_notifications()?; + Ok(result) + } + fn cancel_automatic_check(&mut self) { + if let Some((job, _, _)) = self.auto_update_job.take() { + let _ = self.automatic_updater.jobs.cancel(&job); + } + self.auto_update_error = None; + } + fn reset_automatic_updates(&mut self) { + self.cancel_automatic_check(); + self.db.update_notice = None; + self.notifications.retain(|_, n| n.update.is_none()); + self.db.notification_inbox.retain(|_, n| n.update.is_none()); + self.next_update_check = Instant::now() + Duration::from_secs(30); + process::event(&self.events, json!({"kind":"notification.changed"})); + } + fn automatic_update_check(&mut self) { + if let Some((job, source, channel)) = self.auto_update_job.clone() { + if let Ok(result) = self.automatic_updater.jobs.status(&job) { + if matches!( + result["phase"].as_str(), + Some("done" | "failed" | "cancelled") + ) { + self.auto_update_job = None; + self.last_update_check = Some(now_ms()); + self.auto_update_error = result["error"].as_str().map(str::to_owned); + if result["phase"] == "done" + && self.db.update_check.enabled + && self.db.update_source.as_ref() == Some(&source) + && self.db.update_channel == channel + { + if let Err(e) = self.announce_update(&result["result"], source, channel) { + self.auto_update_error = Some(e.to_string()); + } + } + } + } else { + self.auto_update_job = None; + } + } + if !self.db.update_check.enabled + || self.auto_update_job.is_some() + || Instant::now() < self.next_update_check + || self.updater.ensure_idle().is_err() + { + return; + } + let Some(source) = self.db.update_source.clone() else { + return; + }; + let installing = fs::read(self.root.join("update-status.json")) + .ok() + .and_then(|b| serde_json::from_slice::(&b).ok()) + .is_some_and(|v| v["phase"] == "installing"); + if installing { + return; + } + self.next_update_check = + Instant::now() + Duration::from_secs(self.db.update_check.interval_hours * 3600); + match self.automatic_updater.check( + source.clone(), + self.db.update_channel, + self.db.proxy.clone(), + ) { + Ok(job) => { + self.auto_update_job = Some(( + job["job"].as_str().unwrap().into(), + source, + self.db.update_channel, + )); + self.auto_update_error = None; + } + Err(e) => { + self.auto_update_error = Some(e.to_string()); + self.last_update_check = Some(now_ms()); + } + } + } + fn announce_update( + &mut self, + result: &Value, + source: UpdateSource, + channel: UpdateChannel, + ) -> Result<()> { + let Some(version) = result["release"]["version"].as_str() else { + return Ok(()); + }; + let current = fs::read_to_string(self.root.join("current/VERSION")) + .unwrap_or_else(|_| env!("CARGO_PKG_VERSION").into()); + if crate::update::descriptor_version(version)? + .cmp_precedence(&crate::update::descriptor_version(current.trim())?) + != std::cmp::Ordering::Greater + { + return Ok(()); + } + let notice = UpdateNotice { + source, + channel, + version: version.into(), + }; + if self.db.update_notice.as_ref() == Some(¬ice) { + return Ok(()); + } + let notification: Notification = serde_json::from_value( + json!({"id":"framely.update","title":format!("Framely {version} is available"),"body":"Open Updates to review and install this version, or ignore this reminder.","inbox":true,"durationMs":15000,"actions":[{"id":"ignore","label":"Ignore"},{"id":"open","label":"Open updates"}]}), + )?; + self.insert_notification("", "Framely", notification, Some(notice.clone()))?; + self.db.update_notice = Some(notice); + self.save() } fn install(&mut self, p: Value) -> Result { ensure!( @@ -1914,3 +2274,435 @@ mod request_tests { assert_eq!(worker.join().unwrap(), json!(false)); } } + +#[cfg(test)] +mod notification_tests { + use super::*; + fn plugin(core: &mut Service) { + core.db.plugins.insert("test.notice".into(), serde_json::from_value(json!({"manifest":{"schemaVersion":1,"apiVersion":1,"id":"test.notice","name":"Notice tests","author":"Framely","version":"1.0.0","ui":{"quickPage":"page.js"},"files":{}},"enabled":true,"favorite":false,"order":0,"source":null})).unwrap()); + } + fn send(core: &mut Service, id: &str, extra: Value) { + let mut n = json!({"id":id,"title":"Download finished","body":"Ready to open","actions":[{"id":"open","label":"Open"}]}); + for (k, v) in extra.as_object().unwrap() { + n[k] = v.clone(); + } + core.handle( + "notification.send", + json!({"plugin":"test.notice","notification":n}), + ) + .unwrap(); + } + fn action(core: &mut Service, id: &str) -> Result { + core.handle( + "notification.action", + json!({"plugin":"test.notice","id":id,"action":"open"}), + ) + } + #[test] + fn popup_settings_preserve_suppressed_messages_and_survive_reload() { + let root = tempfile::tempdir().unwrap(); + let mut core = crate::tests::accepted_service(root.path(), 1000).unwrap(); + plugin(&mut core); + assert!(core.db.notification_settings.popups); + assert!(core.db.notification_settings.badge); + send(&mut core, "visible", json!({})); + core.handle("notification.settings.save", json!({"popups":false})) + .unwrap(); + assert!(!core.notifications["test.notice:visible"].toast); + assert!(core.notifications["test.notice:visible"].in_inbox); + send(&mut core, "silent", json!({})); + let status = core.handle("status", json!({})).unwrap(); + assert_eq!(status["notifications"].as_array().unwrap().len(), 0); + assert_eq!(status["inbox"].as_array().unwrap().len(), 2); + assert_eq!(status["notificationBadge"], 2); + let mut reloaded = Service::load(root.path(), 1000).unwrap(); + assert!(!reloaded.db.notification_settings.popups); + assert_eq!(reloaded.notification_badge(), 2); + reloaded + .handle("notification.settings.save", json!({"popups":true})) + .unwrap(); + assert!(reloaded.notifications.values().all(|n| !n.toast)); + send(&mut reloaded, "next", json!({})); + assert!(reloaded.notifications["test.notice:next"].toast); + assert!(!reloaded.notifications["test.notice:next"].in_inbox); + } + #[test] + fn plugin_notification_preferences_are_independent_and_global_switches_take_priority() { + let root = tempfile::tempdir().unwrap(); + let mut core = crate::tests::accepted_service(root.path(), 1000).unwrap(); + plugin(&mut core); + core.handle( + "notification.settings.save", + json!({"plugin":"test.notice","popups":false}), + ) + .unwrap(); + core.handle( + "notification.settings.save", + json!({"plugin":"test.notice","badge":false}), + ) + .unwrap(); + send(&mut core, "silent", json!({})); + assert!(!core.notifications["test.notice:silent"].toast); + assert!(core.notifications["test.notice:silent"].in_inbox); + assert_eq!(core.notification_badge(), 0); + core.handle( + "notification.settings.save", + json!({"plugin":"test.notice","badge":true}), + ) + .unwrap(); + assert_eq!(core.notification_badge(), 1); + core.handle( + "notification.settings.save", + json!({"badge":false,"popups":false}), + ) + .unwrap(); + core.handle( + "notification.settings.save", + json!({"plugin":"test.notice","popups":true}), + ) + .unwrap(); + send(&mut core, "global", json!({})); + assert!(!core.notifications["test.notice:global"].toast); + assert_eq!(core.notification_badge(), 0); + assert!(core.db.notification_settings.plugins["test.notice"].badge); + core.handle("notification.settings.save", json!({"badge":true})) + .unwrap(); + assert_eq!(core.notification_badge(), 2); + assert!(core + .handle( + "notification.settings.save", + json!({"plugin":"missing","badge":false}) + ) + .is_err()); + assert!(core + .handle("notification.settings.save", json!({"badge":"false"})) + .is_err()); + assert!(core + .handle("notification.settings.save", json!({"unknown":true})) + .is_err()); + assert_eq!(core.notification_badge(), 2); + } + #[test] + fn framely_notification_preferences_control_update_reminders_and_survive_reload() { + let root = tempfile::tempdir().unwrap(); + let mut core = crate::tests::accepted_service(root.path(), 1000).unwrap(); + plugin(&mut core); + send(&mut core, "saved", json!({"inbox":true})); + let source = UpdateSource { + url: "https://example.org/framely-release.json".into(), + }; + core.db.update_source = Some(source.clone()); + core.announce_update( + &json!({"release":{"version":"0.5.0-abcdef012345"}}), + source.clone(), + UpdateChannel::Stable, + ) + .unwrap(); + assert!(core.notifications[":framely.update"].toast); + assert_eq!(core.notification_badge(), 2); + core.handle( + "notification.settings.save", + json!({"plugin":"","popups":false,"badge":false}), + ) + .unwrap(); + assert!(core.db.notification_settings.popups && core.db.notification_settings.badge); + assert!(!core.notifications[":framely.update"].toast); + assert!(core.notifications[":framely.update"].in_inbox); + assert!(core.notifications["test.notice:saved"].toast); + assert_eq!(core.notification_badge(), 1); + core.announce_update( + &json!({"release":{"version":"0.5.1-abcdef012345"}}), + source, + UpdateChannel::Stable, + ) + .unwrap(); + assert!(!core.notifications[":framely.update"].toast); + assert!(core.notifications[":framely.update"].in_inbox); + let mut core = Service::load(root.path(), 1000).unwrap(); + assert!(!core.db.notification_settings.plugins[""].popups); + assert!(!core.db.notification_settings.plugins[""].badge); + assert_eq!(core.notification_badge(), 1); + core.handle( + "notification.settings.save", + json!({"plugin":"","badge":true}), + ) + .unwrap(); + assert_eq!(core.notification_badge(), 2); + assert!(!core.notifications[":framely.update"].toast); + core.handle("notification.settings.save", json!({"badge":false})) + .unwrap(); + assert_eq!(core.notification_badge(), 0); + assert!(core.db.notification_settings.plugins[""].badge); + } + #[test] + fn reading_inbox_clears_badge_without_removing_messages_or_reading_replacements() { + let root = tempfile::tempdir().unwrap(); + let mut core = crate::tests::accepted_service(root.path(), 1000).unwrap(); + plugin(&mut core); + send(&mut core, "saved", json!({"inbox":true})); + let old = core.notifications["test.notice:saved"].created_at; + send(&mut core, "saved", json!({"inbox":true})); + core.handle( + "notification.read", + json!({"entries":[{"plugin":"test.notice","id":"saved","createdAt":old}]}), + ) + .unwrap(); + assert_eq!(core.notification_badge(), 1); + let current = core.notifications["test.notice:saved"].created_at; + core.handle( + "notification.read", + json!({"entries":[{"plugin":"test.notice","id":"saved","createdAt":current}]}), + ) + .unwrap(); + assert_eq!(core.notification_badge(), 0); + assert!(core.notifications["test.notice:saved"].toast); + assert_eq!(core.db.notification_inbox.len(), 1); + let reloaded = Service::load(root.path(), 1000).unwrap(); + assert_eq!(reloaded.notification_badge(), 0); + assert!(reloaded.db.notification_inbox["test.notice:saved"].read); + } + #[test] + fn notifications_opt_in_to_persistent_inbox_and_default_actions_remove() { + let root = tempfile::tempdir().unwrap(); + let mut core = crate::tests::accepted_service(root.path(), 1000).unwrap(); + plugin(&mut core); + send(&mut core, "transient", json!({})); + send(&mut core, "saved", json!({"inbox":true})); + assert_eq!(core.db.notification_inbox.len(), 1); + core.notifications + .get_mut("test.notice:saved") + .unwrap() + .expires_at = Some(0); + core.notifications + .get_mut("test.notice:transient") + .unwrap() + .expires_at = Some(0); + core.expire_notifications(); + assert!(!core.notifications.contains_key("test.notice:transient")); + assert!(!core.notifications["test.notice:saved"].toast); + let mut reloaded = Service::load(root.path(), 1000).unwrap(); + assert_eq!(reloaded.notifications.len(), 1); + assert!(!reloaded.notifications["test.notice:saved"].toast); + action(&mut reloaded, "saved").unwrap(); + assert!(reloaded.notifications.is_empty()); + assert!(Service::load(root.path(), 1000) + .unwrap() + .db + .notification_inbox + .is_empty()); + } + #[test] + fn notification_action_policies_are_independent_and_stale_actions_are_rejected() { + let root = tempfile::tempdir().unwrap(); + let mut core = crate::tests::accepted_service(root.path(), 1000).unwrap(); + plugin(&mut core); + for close in [false, true] { + for remove in [false, true] { + let id = format!("policy.{close}.{remove}"); + send( + &mut core, + &id, + json!({"inbox":true,"durationMs":0,"actions":[{"id":"open","label":"Open","closeOnClick":close,"removeFromInboxOnClick":remove}]}), + ); + action(&mut core, &id).unwrap(); + let entry = core.notifications.get(&format!("test.notice:{id}")); + assert_eq!(entry.is_some(), !close || !remove); + if let Some(entry) = entry { + assert_eq!(entry.toast, !close); + assert_eq!(entry.in_inbox, !remove); + assert!(entry.expires_at.is_none()); + } + } + } + send(&mut core, "replace", json!({"inbox":true})); + let old = core.notifications["test.notice:replace"].created_at; + send(&mut core, "replace", json!({"inbox":true})); + assert!(core + .handle( + "notification.action", + json!({"plugin":"test.notice","id":"replace","action":"open","createdAt":old}) + ) + .is_err()); + assert!(core.notifications["test.notice:replace"].toast); + } + #[test] + fn dismiss_and_disable_keep_inbox_but_removal_drops_it() { + let root = tempfile::tempdir().unwrap(); + let mut core = crate::tests::accepted_service(root.path(), 1000).unwrap(); + plugin(&mut core); + send(&mut core, "saved", json!({"inbox":true})); + core.handle( + "notification.dismiss", + json!({"plugin":"test.notice","id":"saved"}), + ) + .unwrap(); + assert!(!core.notifications["test.notice:saved"].toast); + assert!(core.notifications["test.notice:saved"].in_inbox); + core.cleanup_resources("test.notice"); + core.db.plugins.get_mut("test.notice").unwrap().enabled = false; + assert!(action(&mut core, "saved").is_err()); + assert!(core.notifications.contains_key("test.notice:saved")); + core.handle( + "notification.remove", + json!({"plugin":"test.notice","id":"saved"}), + ) + .unwrap(); + assert!(core.db.notification_inbox.is_empty()); + } + #[test] + fn failed_notification_backend_keeps_message_for_retry() { + let root = tempfile::tempdir().unwrap(); + let mut core = crate::tests::accepted_service(root.path(), 1000).unwrap(); + plugin(&mut core); + crate::process::TEST_TOOLS + .with(|p| *p.borrow_mut() = Some(root.path().join("missing-tools"))); + send(&mut core, "saved", json!({"inbox":true})); + core.db + .plugins + .get_mut("test.notice") + .unwrap() + .manifest + .backend = + Some(serde_json::from_value(json!({"entry":"missing","runAs":"steamos"})).unwrap()); + assert!(action(&mut core, "saved").is_err()); + assert!(core.notifications["test.notice:saved"].toast); + assert!(core.db.notification_inbox.contains_key("test.notice:saved")); + crate::process::TEST_TOOLS.with(|p| *p.borrow_mut() = None); + } + #[test] + fn automatic_update_notifications_ignore_old_versions_and_deduplicate_after_reload() { + let root = tempfile::tempdir().unwrap(); + let mut core = crate::tests::accepted_service(root.path(), 1000).unwrap(); + let source = UpdateSource { + url: "https://example.org/framely-release.json".into(), + }; + core.db.update_source = Some(source.clone()); + for version in ["0.1.0", "0.4.2-preview.12-012345abcdef"] { + core.announce_update( + &json!({"release":{"version":version}}), + source.clone(), + UpdateChannel::Stable, + ) + .unwrap(); + assert!(core.notifications.is_empty()); + } + let result = json!({"release":{"version":"0.5.0-abcdef012345"}}); + core.announce_update(&result, source.clone(), UpdateChannel::Stable) + .unwrap(); + assert_eq!(core.notifications.len(), 1); + core.handle( + "notification.action", + json!({"id":"framely.update","action":"ignore"}), + ) + .unwrap(); + assert!(core.notifications.is_empty()); + let mut core = Service::load(root.path(), 1000).unwrap(); + core.announce_update(&result, source.clone(), UpdateChannel::Stable) + .unwrap(); + assert!(core.notifications.is_empty()); + core.announce_update( + &json!({"release":{"version":"0.5.1-abcdef012345"}}), + source, + UpdateChannel::Stable, + ) + .unwrap(); + assert_eq!( + core.handle( + "notification.action", + json!({"id":"framely.update","action":"open"}) + ) + .unwrap(), + json!({"openUpdates":true}) + ); + assert!(core + .events + .lock() + .unwrap() + .iter() + .any(|e| e["kind"] == "manager.open" && e["page"] == "updates")); + assert!(core.db.notification_inbox.is_empty()); + } + #[test] + fn automatic_update_schedule_settings_and_stale_completions() { + let root = tempfile::tempdir().unwrap(); + let mut core = crate::tests::accepted_service(root.path(), 1000).unwrap(); + let source = UpdateSource { + url: "https://example.org/framely-release.json".into(), + }; + core.db.update_source = Some(source.clone()); + assert!(core.db.update_check.enabled); + assert_eq!(core.db.update_check.interval_hours, 6); + core.handle( + "system.check.settings.save", + json!({"settings":{"enabled":false,"intervalHours":12}}), + ) + .unwrap(); + core.next_update_check = Instant::now(); + core.automatic_update_check(); + assert!(core.auto_update_job.is_none()); + assert!( + !Service::load(root.path(), 1000) + .unwrap() + .db + .update_check + .enabled + ); + for hours in [0, 169] { + assert!(core + .handle( + "system.check.settings.save", + json!({"settings":{"enabled":true,"intervalHours":hours}}) + ) + .is_err()); + } + core.db.update_check.enabled = true; + core.next_update_check = Instant::now() + Duration::from_secs(3600); + core.automatic_update_check(); + assert!(core.auto_update_job.is_none()); + // Inject a completed background job to exercise the timer's completion path + // without any external network request or UI polling. + let job = core + .automatic_updater + .jobs + .task("system.check", |_| { + Ok(json!({"release":{"version":"0.5.0-abcdef012345"}})) + }) + .unwrap(); + let id = job["job"].as_str().unwrap().to_owned(); + for _ in 0..100 { + if core.automatic_updater.jobs.status(&id).unwrap()["phase"] == "done" { + break; + } + std::thread::sleep(Duration::from_millis(5)); + } + core.auto_update_job = Some((id, source.clone(), UpdateChannel::Stable)); + core.automatic_update_check(); + assert_eq!(core.db.notification_inbox.len(), 1); + assert!(core.last_update_check.is_some()); + core.handle("system.channel.save", json!({"channel":"testing"})) + .unwrap(); + assert!(core.db.notification_inbox.is_empty()); + assert!(core.db.update_notice.is_none()); + // A result from the previous channel arriving late cannot recreate its reminder. + core.auto_update_job = Some(( + job["job"].as_str().unwrap().into(), + source, + UpdateChannel::Stable, + )); + core.automatic_update_check(); + assert!(core.db.notification_inbox.is_empty()); + // A due check advances the configured timer even if source validation fails; + // this exercises failure/retry scheduling without contacting a server. + core.db.update_source = Some(UpdateSource { + url: "file:///invalid-release.json".into(), + }); + core.next_update_check = Instant::now(); + core.automatic_update_check(); + assert!(core.auto_update_error.is_some()); + assert!(core.auto_update_job.is_none()); + assert!( + core.next_update_check.duration_since(Instant::now()) + > Duration::from_secs(12 * 3600 - 2) + ); + } +} diff --git a/src/session.rs b/src/session.rs index 4915ce1..3751804 100644 --- a/src/session.rs +++ b/src/session.rs @@ -358,7 +358,10 @@ impl Agent { commands.push(queue.pop_front().unwrap()); } drop(queue); - return send_json(r, json!({"commands":commands})); + let badge = self + .core("host.notification.badge", json!({})) + .unwrap_or(json!(0)); + return send_json(r, json!({"commands":commands,"notificationBadge":badge})); } if let Some(rel) = path.strip_prefix("/plugin-assets/") { let (id, file) = rel.split_once('/').context("Invalid asset path")?; @@ -696,8 +699,13 @@ impl Agent { _ => "menu.close", }; let mut command = json!({"kind":kind}); - if method == "host.manager.open" && p["page"] == "catalog" { - command["page"] = json!("catalog"); + if method == "host.manager.open" + && matches!( + p["page"].as_str(), + Some("catalog" | "updates" | "notification-settings") + ) + { + command["page"] = p["page"].clone(); } push(&self.commands, command); Ok(json!(true)) @@ -787,8 +795,11 @@ impl Agent { | "window.open" | "window.close" | "notification.send" + | "notification.dismiss" | "notification.remove" | "notification.action" + | "notification.settings.save" + | "notification.read" | "sources.save" | "subscriptions.add" | "subscriptions.change" @@ -802,6 +813,7 @@ impl Agent { | "safeMode" | "system.source.save" | "system.channel.save" + | "system.check.settings.save" | "system.check.start" | "system.download.start" | "system.job.status" @@ -1115,6 +1127,7 @@ pub fn serve( "window.close", "plugin.disabled", "notification.changed", + "manager.open", ] .contains(&e["kind"].as_str().unwrap_or("")) { @@ -1473,12 +1486,14 @@ mod tests { }) } #[test] - fn manager_store_entry_preserves_catalog_target_and_rejects_arbitrary_pages() { + fn manager_entry_preserves_catalog_and_update_targets_and_rejects_arbitrary_pages() { let root = tempfile::tempdir().unwrap(); let server = Server::http("127.0.0.1:0").unwrap(); let agent = agent(&server, root.path()); for params in [ json!({"page":"catalog"}), + json!({"page":"updates"}), + json!({"page":"notification-settings"}), json!({}), json!({"page":"https://example.org"}), ] { @@ -1488,8 +1503,13 @@ mod tests { } let commands = agent.commands.lock().unwrap(); assert_eq!(commands[0], json!({"kind":"manager.open","page":"catalog"})); - assert_eq!(commands[1], json!({"kind":"manager.open"})); - assert_eq!(commands[2], json!({"kind":"manager.open"})); + assert_eq!(commands[1], json!({"kind":"manager.open","page":"updates"})); + assert_eq!( + commands[2], + json!({"kind":"manager.open","page":"notification-settings"}) + ); + assert_eq!(commands[3], json!({"kind":"manager.open"})); + assert_eq!(commands[4], json!({"kind":"manager.open"})); } #[test] fn declining_agreement_only_closes_current_view() { @@ -1871,7 +1891,11 @@ mod tests { let listener = std::os::unix::net::UnixListener::bind(&agent.socket).unwrap(); let socket_worker = std::thread::spawn(move || { let mut core = crate::service::Service::load(&root.path().join("state"), 1000).unwrap(); - for expected in ["host.ui.visibility", "agreement.status"] { + for expected in [ + "host.ui.visibility", + "host.notification.badge", + "agreement.status", + ] { let (mut stream, _) = listener.accept().unwrap(); let request = ipc::read(&mut stream).unwrap(); assert_eq!(request["method"], expected); @@ -2244,7 +2268,7 @@ mod tests { .unwrap(); print!("{}", String::from_utf8_lossy(&store_result.stdout)); eprint!("{}", String::from_utf8_lossy(&store_result.stderr)); - agent.core("notification.send",json!({"plugin":"test.bridge","notification":{"id":"toast","title":"Toast integration","body":"Compact notification body","image":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN1kAAAAASUVORK5CYII=","durationMs":15000,"actions":[{"id":"ok","label":"确认","icon":"✓"},{"id":"save","label":"保存","icon":"↓"},{"id":"more","label":"详情","icon":"⋯"}]}})).unwrap(); + agent.core("notification.send",json!({"plugin":"test.bridge","notification":{"id":"toast","title":"Toast integration","body":"Compact notification body","image":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aN1kAAAAASUVORK5CYII=","durationMs":15000,"actions":[{"id":"ok","label":"确认","icon":"✓","closeOnClick":false,"removeFromInboxOnClick":false},{"id":"save","label":"保存","icon":"↓"},{"id":"more","label":"详情","icon":"⋯"}]}})).unwrap(); let notification_result = Command::new(std::env::var_os("FRAMELY_BROWSER_PROBE").unwrap()) .arg(format!( "{}/boot/{}?view=notifications", diff --git a/src/tests.rs b/src/tests.rs index 25fbcce..8ddf839 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -299,6 +299,7 @@ fn notification_limits_and_sources() { image: None, actions: vec![], duration_ms: 8000, + inbox: false, }; n.validate().unwrap(); let mut invalid = n.clone(); @@ -307,6 +308,8 @@ fn notification_limits_and_sources() { id: i.to_string(), label: "x".into(), icon: "x".into(), + close_on_click: true, + remove_from_inbox_on_click: true, }) .collect(); assert!(invalid.validate().is_err()); diff --git a/src/update.rs b/src/update.rs index 3777ed7..14d0f78 100644 --- a/src/update.rs +++ b/src/update.rs @@ -150,7 +150,7 @@ struct GitHubRelease { draft: bool, assets: Vec, } -fn descriptor_version(version: &str) -> Result { +pub(crate) fn descriptor_version(version: &str) -> Result { // Runtime VERSION appends a twelve-digit content hash to the Cargo version. let version = version .rsplit_once('-') diff --git a/tests/browser_probe.cpp b/tests/browser_probe.cpp index 87393e5..a5fe28b 100644 --- a/tests/browser_probe.cpp +++ b/tests/browser_probe.cpp @@ -37,6 +37,7 @@ int main(int argc,char** argv){ client.api.get_render_handler=[](cef_client_t*){return render.acquire();};client.api.get_display_handler=[](cef_client_t*){return display.acquire();};client.api.get_life_span_handler=[](cef_client_t*){return life.acquire();}; cef_window_info_t win{};win.size=sizeof(win);win.windowless_rendering_enabled=1;cef_browser_settings_t bs{};bs.size=sizeof(bs);bs.windowless_frame_rate=30;cef_string_t uri{};str(uri,url);auto* browser=cef_browser_host_create_browser_sync(&win,&client.api,&uri,&bs,nullptr,nullptr);cef_string_utf16_clear(&uri);if(!browser)return 4;auto* host=browser->get_host(browser); auto end=std::chrono::steady_clock::now()+std::chrono::seconds(20);bool injected=false; - while(std::chrono::steady_clock::now()get_main_frame(browser);cef_string_t code{},source{};str(code,install_review_mode?"window.runInstallReviewChecks()":notification_mode?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));for(let i=0;i<100&&!document.querySelector('.toast');i++)await wait(30);if(innerWidth!==600||innerHeight!==360||devicePixelRatio!==2)throw Error('notification dimensions');const toast=document.querySelector('.toast');if(!toast?.textContent.includes('Toast integration'))throw Error('notification missing');if(toast.getBoundingClientRect().height>240)throw Error('notification too large');if(toast.querySelectorAll('.toast-actions button').length!==3)throw Error('actions missing');toast.querySelector('[aria-label="确认"]').click();await wait(150);document.querySelector('[aria-label="关闭通知"]').click();await wait(150);const status=await fetch('/api',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({method:'status',params:{}})}).then(r=>r.json());if(status.result.notifications.length)throw Error('notification not withdrawn');console.log('FRAMELY_BRIDGE_PASS');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS":std::getenv("FRAMELY_REMOTE_TEST")?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));await wait(300);const button=document.createElement('button');button.style.cssText='position:fixed;left:20px;top:20px;width:100px;height:50px;z-index:9999';button.onclick=()=>console.log('FRAMELY_DPI_CLICK_PASS');document.body.append(button);console.log('FRAMELY_DPI_CLICK_REQUEST');await wait(200);button.remove();for(let i=0;i<100;i++){const plugin=document.querySelector('.plugin-main');if(plugin){plugin.click();return;}await wait(50);}throw Error('remote plugin list timeout');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS":std::getenv("FRAMELY_STORE_TEST")?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));const until=async find=>{for(let i=0;i<160;i++){const value=find();if(value)return value;await wait(50);}throw Error("store timeout "+document.body.innerText);};await wait(300);if(innerWidth!==1440||innerHeight!==810||devicePixelRatio!==4)throw Error("manager layout or DPI mismatch");[...document.querySelectorAll("nav button")].find(b=>b.textContent==="插件源").click();await wait(200);if(!document.body.textContent.includes("Subscription fixture"))throw Error("subscription settings missing");const autoRefresh=[...document.querySelectorAll("button")].find(b=>b.textContent==="关闭自动刷新");if(!autoRefresh)throw Error("subscription automatic refresh missing");autoRefresh.click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="开启自动刷新"));[...document.querySelectorAll("nav button")].find(b=>b.textContent==="关于").click();await wait(150);const updates=document.querySelector(".system-updates");if(!updates||updates.querySelector("input"))throw Error("release configuration leaked into user settings");if(!updates.textContent.includes("尚未配置更新服务"))throw Error("missing unconfigured update service message");const check=[...updates.querySelectorAll("button")].find(b=>b.textContent==="检查更新");if(!check?.disabled)throw Error("unconfigured check enabled");[...document.querySelectorAll("button")].find(b=>b.textContent==="查看协议与隐私声明").click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="拒绝协议"));document.querySelector('.language-picker button').click();const english=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent==='English'));english.click();await until(()=>document.querySelector('h1')?.textContent==='Before using Framely');document.querySelector('.language-picker button').click();const chinese=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent==='简体中文'));chinese.click();await until(()=>[...document.querySelectorAll('button')].find(b=>b.textContent==='拒绝协议'));[...document.querySelectorAll("button")].find(b=>b.textContent==="拒绝协议").click();await until(()=>document.body.textContent.includes("拒绝协议将停用全部插件"));[...document.querySelectorAll("button")].find(b=>b.textContent==="取消").click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="返回关于"));[...document.querySelectorAll("button")].find(b=>b.textContent==="返回关于").click();await until(()=>document.querySelector(".system-updates"));[...document.querySelectorAll("nav button")].find(b=>b.textContent.includes("插件库")).click();const card=await until(()=>document.querySelector(".store-card"));if(!card.textContent.includes("可更新"))throw Error("update not detected");if(card.querySelector(".plugin-avatar").getBoundingClientRect().width<64)throw Error("store icon not enlarged");if(document.querySelectorAll(".store-card").length!==1)throw Error("versions duplicated plugin cards");document.querySelector(".framely-select button").click();const tag=await until(()=>[...document.querySelectorAll("[role=option]")].find(b=>b.textContent==="工具"));if(!tag)throw Error("tag missing");tag.click();await wait(100);if(document.querySelector(".store-card .run-user-badge")?.textContent!=="root")throw Error("store run user missing");document.querySelector(".store-card").click();await wait(100);if(!document.body.textContent.includes("Store integration details"))throw Error("detail missing");if(!document.querySelector(".store-detail .run-user-notice.is-root"))throw Error("store root warning missing");console.log("FRAMELY_PREVIEW_STORE");document.querySelector(".detail-heading button").click();const approve=await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="确认安装"));if(document.querySelector(".modal h2")?.textContent!=="更新 Bridge")throw Error("manual install update title missing");if(document.querySelector(".install-review .install-account")?.textContent!=="steamos")throw Error("install must use package identity");if(document.body.textContent.includes("权限变化"))throw Error("unexpected privilege change");approve.click();await until(()=>!document.querySelector(".modal"));const status=await fetch("/api",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({method:"status",params:{}})}).then(r=>r.json());if(status.result.database.plugins["test.bridge"].manifest.version!=="2.0.0")throw Error("install not persisted");const chooseVersion=async v=>{document.querySelector('.store-detail .framely-select button').click();const option=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent.split(' · ')[0]===v));option.click();await wait(100);if(document.querySelector('.store-detail .install-account')?.textContent!==(v==='2.0.0'?'root':'steamos'))throw Error('selected version user incorrect');document.querySelector('.detail-heading button').click();const approveVersion=await until(()=>[...document.querySelectorAll('button')].find(b=>b.textContent==='确认安装'));approveVersion.click();await until(()=>!document.querySelector('.modal'));const result=await fetch('/api',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({method:'status',params:{}})}).then(r=>r.json());if(result.result.database.plugins['test.bridge'].manifest.version!==v)throw Error('selected version not installed '+v);};await chooseVersion('1');await chooseVersion('2.0.0');console.log('FRAMELY_VERSION_SELECTION_PASS');[...document.querySelectorAll("nav button")].find(b=>b.textContent==="插件源").click();const removeSub=await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="删除订阅"));removeSub.click();const confirm=await until(()=>[...document.querySelectorAll(".modal button")].find(b=>b.textContent==="确认"));confirm.click();await until(()=>!document.body.textContent.includes("Subscription fixture"));console.log("FRAMELY_BRIDGE_PASS");})().catch(e=>console.error("FRAMELY_BRIDGE_FAIL "+e)))JS":R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));await wait(300);if(innerWidth!==600||innerHeight!==840||devicePixelRatio!==2)throw Error('quick layout or DPI changed');const dpiButton=document.createElement('button');dpiButton.style.cssText='position:fixed;left:20px;top:20px;width:100px;height:50px;z-index:9999';dpiButton.textContent='DPI test';let clicked=false;dpiButton.onclick=()=>{clicked=true;console.log('FRAMELY_DPI_CLICK_PASS');};document.body.append(dpiButton);console.log('FRAMELY_DPI_CLICK_REQUEST');await wait(200);dpiButton.remove();if(!clicked)throw Error('logical pointer coordinates failed');const originalFetch=window.fetch;let pulses=0;window.fetch=function(url,init){if(init?.body&&JSON.parse(init.body).method==='host.haptic')pulses++;return originalFetch.call(this,url,init);};const fixture=document.createElement('div');fixture.innerHTML='';document.body.append(fixture);const children=fixture.querySelectorAll('span'),buttons=fixture.querySelectorAll('button');const over=e=>e.dispatchEvent(new PointerEvent('pointerover',{bubbles:true}));const leave=(e,target)=>e.dispatchEvent(new PointerEvent('pointerout',{bubbles:true,relatedTarget:target}));over(children[0]);leave(children[0],children[1]);over(children[1]);leave(children[1],children[2]);over(children[2]);leave(children[2],buttons[2]);over(buttons[2]);await wait(100);fixture.remove();window.fetch=originalFetch;if(pulses!==2)throw Error('hover count='+pulses);console.log('FRAMELY_HOVER_PASS');console.log('FRAMELY_PREVIEW_FAVORITES');[...document.querySelectorAll('nav button')].find(b=>b.textContent==='设置').click();await wait(200);console.log('FRAMELY_PREVIEW_SETTINGS');if(!document.querySelector('.quick-settings').firstElementChild.classList.contains('setting-entry'))throw Error('manager entry is below language settings');document.querySelector('.setting-entry').click();await wait(100);[...document.querySelectorAll('nav button')].find(b=>b.textContent==='已安装').click();await wait(200);console.log('FRAMELY_PREVIEW_INSTALLED');for(let i=0;i<100;i++){await wait(50);const b=document.querySelector('.plugin-main');if(b){b.click();return;}}console.error('FRAMELY_BRIDGE_FAIL manager plugin list timeout');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS");f->execute_java_script(f,&code,&source,1);cef_string_utf16_clear(&code);f->base.release(&f->base);injected=true;}std::this_thread::sleep_for(std::chrono::milliseconds(5));} + while(std::chrono::steady_clock::now()get_main_frame(browser);cef_string_t code{},source{};str(code,install_review_mode?"window.runInstallReviewChecks()":notification_mode?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));for(let i=0;i<100&&!document.querySelector('.toast');i++)await wait(30);if(innerWidth!==600||innerHeight!==360||devicePixelRatio!==2)throw Error('notification dimensions');const toast=document.querySelector('.toast');if(!toast?.textContent.includes('Toast integration'))throw Error('notification missing');if(toast.getBoundingClientRect().height>240)throw Error('notification too large');if(toast.querySelectorAll('.toast-actions button').length!==3)throw Error('actions missing');toast.querySelector('[aria-label="确认"]').click();await wait(150);document.querySelector('[aria-label="关闭通知"]').click();await wait(150);const status=await fetch('/api',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({method:'status',params:{}})}).then(r=>r.json());if(status.result.notifications.length)throw Error('notification not withdrawn');console.log('FRAMELY_BRIDGE_PASS');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS":std::getenv("FRAMELY_REMOTE_TEST")?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));await wait(300);const button=document.createElement('button');button.style.cssText='position:fixed;left:20px;top:20px;width:100px;height:50px;z-index:9999';button.onclick=()=>console.log('FRAMELY_DPI_CLICK_PASS');document.body.append(button);console.log('FRAMELY_DPI_CLICK_REQUEST');await wait(200);button.remove();for(let i=0;i<100;i++){const plugin=document.querySelector('.plugin-main');if(plugin){plugin.click();return;}await wait(50);}throw Error('remote plugin list timeout');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS":std::getenv("FRAMELY_STORE_TEST")?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));const until=async find=>{for(let i=0;i<160;i++){const value=find();if(value)return value;await wait(50);}throw Error("store timeout "+document.body.innerText);};await wait(300);if(innerWidth!==1440||innerHeight!==810||devicePixelRatio!==4)throw Error("manager layout or DPI mismatch");[...document.querySelectorAll("nav button")].find(b=>b.textContent==="插件源").click();await wait(200);if(!document.body.textContent.includes("Subscription fixture"))throw Error("subscription settings missing");const autoRefresh=[...document.querySelectorAll("button")].find(b=>b.textContent==="关闭自动刷新");if(!autoRefresh)throw Error("subscription automatic refresh missing");autoRefresh.click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="开启自动刷新"));[...document.querySelectorAll("nav button")].find(b=>b.textContent==="关于").click();await wait(150);const updates=document.querySelector(".system-updates");if(!updates||updates.querySelector("input:not([type=checkbox]),textarea"))throw Error("release configuration leaked into user settings");if(!updates.querySelector(".automatic-update-settings input[type=checkbox]")?.disabled)throw Error("automatic check controls enabled without source");if(!updates.textContent.includes("尚未配置更新服务"))throw Error("missing unconfigured update service message");const check=[...updates.querySelectorAll("button")].find(b=>b.textContent==="检查更新");if(!check?.disabled)throw Error("unconfigured check enabled");[...document.querySelectorAll("button")].find(b=>b.textContent==="查看协议与隐私声明").click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="拒绝协议"));document.querySelector('.language-picker button').click();const english=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent==='English'));english.click();await until(()=>document.querySelector('h1')?.textContent==='Before using Framely');document.querySelector('.language-picker button').click();const chinese=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent==='简体中文'));chinese.click();await until(()=>[...document.querySelectorAll('button')].find(b=>b.textContent==='拒绝协议'));[...document.querySelectorAll("button")].find(b=>b.textContent==="拒绝协议").click();await until(()=>document.body.textContent.includes("拒绝协议将停用全部插件"));[...document.querySelectorAll("button")].find(b=>b.textContent==="取消").click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="返回关于"));[...document.querySelectorAll("button")].find(b=>b.textContent==="返回关于").click();await until(()=>document.querySelector(".system-updates"));[...document.querySelectorAll("nav button")].find(b=>b.textContent.includes("插件库")).click();const card=await until(()=>document.querySelector(".store-card"));if(!card.textContent.includes("可更新"))throw Error("update not detected");if(card.querySelector(".plugin-avatar").getBoundingClientRect().width<64)throw Error("store icon not enlarged");if(document.querySelectorAll(".store-card").length!==1)throw Error("versions duplicated plugin cards");document.querySelector(".framely-select button").click();const tag=await until(()=>[...document.querySelectorAll("[role=option]")].find(b=>b.textContent==="工具"));if(!tag)throw Error("tag missing");tag.click();await wait(100);if(document.querySelector(".store-card .run-user-badge")?.textContent!=="root")throw Error("store run user missing");document.querySelector(".store-card").click();await wait(100);if(!document.body.textContent.includes("Store integration details"))throw Error("detail missing");if(!document.querySelector(".store-detail .run-user-notice.is-root"))throw Error("store root warning missing");console.log("FRAMELY_PREVIEW_STORE");document.querySelector(".detail-heading button").click();const approve=await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="确认安装"));if(document.querySelector(".modal h2")?.textContent!=="更新 Bridge")throw Error("manual install update title missing");if(document.querySelector(".install-review .install-account")?.textContent!=="steamos")throw Error("install must use package identity");if(document.body.textContent.includes("权限变化"))throw Error("unexpected privilege change");approve.click();await until(()=>!document.querySelector(".modal"));const status=await fetch("/api",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({method:"status",params:{}})}).then(r=>r.json());if(status.result.database.plugins["test.bridge"].manifest.version!=="2.0.0")throw Error("install not persisted");const chooseVersion=async v=>{document.querySelector('.store-detail .framely-select button').click();const option=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent.split(' · ')[0]===v));option.click();await wait(100);if(document.querySelector('.store-detail .install-account')?.textContent!==(v==='2.0.0'?'root':'steamos'))throw Error('selected version user incorrect');document.querySelector('.detail-heading button').click();const approveVersion=await until(()=>[...document.querySelectorAll('button')].find(b=>b.textContent==='确认安装'));approveVersion.click();await until(()=>!document.querySelector('.modal'));const result=await fetch('/api',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({method:'status',params:{}})}).then(r=>r.json());if(result.result.database.plugins['test.bridge'].manifest.version!==v)throw Error('selected version not installed '+v);};await chooseVersion('1');await chooseVersion('2.0.0');console.log('FRAMELY_VERSION_SELECTION_PASS');[...document.querySelectorAll("nav button")].find(b=>b.textContent==="插件源").click();const removeSub=await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="删除订阅"));removeSub.click();const confirm=await until(()=>[...document.querySelectorAll(".modal button")].find(b=>b.textContent==="确认"));confirm.click();await until(()=>!document.body.textContent.includes("Subscription fixture"));console.log("FRAMELY_BRIDGE_PASS");})().catch(e=>console.error("FRAMELY_BRIDGE_FAIL "+e)))JS":R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));await wait(300);if(innerWidth!==600||innerHeight!==840||devicePixelRatio!==2)throw Error('quick layout or DPI changed');const dpiButton=document.createElement('button');dpiButton.style.cssText='position:fixed;left:20px;top:20px;width:100px;height:50px;z-index:9999';dpiButton.textContent='DPI test';let clicked=false;dpiButton.onclick=()=>{clicked=true;console.log('FRAMELY_DPI_CLICK_PASS');};document.body.append(dpiButton);console.log('FRAMELY_DPI_CLICK_REQUEST');await wait(200);dpiButton.remove();if(!clicked)throw Error('logical pointer coordinates failed');const originalFetch=window.fetch;let pulses=0;window.fetch=function(url,init){if(init?.body&&JSON.parse(init.body).method==='host.haptic')pulses++;return originalFetch.call(this,url,init);};const fixture=document.createElement('div');fixture.innerHTML='';document.body.append(fixture);const children=fixture.querySelectorAll('span'),buttons=fixture.querySelectorAll('button');const over=e=>e.dispatchEvent(new PointerEvent('pointerover',{bubbles:true}));const leave=(e,target)=>e.dispatchEvent(new PointerEvent('pointerout',{bubbles:true,relatedTarget:target}));over(children[0]);leave(children[0],children[1]);over(children[1]);leave(children[1],children[2]);over(children[2]);leave(children[2],buttons[2]);over(buttons[2]);await wait(100);fixture.remove();window.fetch=originalFetch;if(pulses!==2)throw Error('hover count='+pulses);console.log('FRAMELY_HOVER_PASS');console.log('FRAMELY_PREVIEW_FAVORITES');[...document.querySelectorAll('nav button')].find(b=>b.textContent==='设置').click();await wait(200);console.log('FRAMELY_PREVIEW_SETTINGS');if(!document.querySelector('.quick-settings').firstElementChild.classList.contains('setting-entry'))throw Error('manager entry is below language settings');document.querySelector('.setting-entry').click();await wait(100);[...document.querySelectorAll('nav button')].find(b=>b.textContent==='已安装').click();await wait(200);console.log('FRAMELY_PREVIEW_INSTALLED');for(let i=0;i<100;i++){await wait(50);const b=document.querySelector('.plugin-main');if(b){b.click();return;}}console.error('FRAMELY_BRIDGE_FAIL manager plugin list timeout');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS");f->execute_java_script(f,&code,&source,1);cef_string_utf16_clear(&code);f->base.release(&f->base);injected=true;}std::this_thread::sleep_for(std::chrono::milliseconds(5));} + if(!passed&&!failed){auto* f=browser->get_main_frame(browser);cef_string_t diagnostic{},source{};str(diagnostic,"console.error('FRAMELY_TIMEOUT '+location.pathname.replace(/\\/boot\\/[^/]+/,'/boot/token')+' '+document.body.innerText.slice(0,400))");f->execute_java_script(f,&diagnostic,&source,1);cef_string_utf16_clear(&diagnostic);f->base.release(&f->base);for(int i=0;i<40;i++){cef_do_message_loop_work();std::this_thread::sleep_for(std::chrono::milliseconds(5));}snapshot("FAILURE");} host->close_browser(host,1);for(int i=0;i<400&&!closed;i++){cef_do_message_loop_work();std::this_thread::sleep_for(std::chrono::milliseconds(5));}bool ok=passed&&!failed&&paints>0&&closed&&scaled_paint&&(notification_mode||rounded_paint)&&(notification_mode||install_review_mode||std::getenv("FRAMELY_STORE_TEST")||dpi_click);std::cout<<"BROWSER_RESULT pass="<base.release(&host->base);browser->base.release(&browser->base);cef_shutdown();return ok?0:5; } diff --git a/tests/notification_badge.cpp b/tests/notification_badge.cpp new file mode 100644 index 0000000..9ce46f4 --- /dev/null +++ b/tests/notification_badge.cpp @@ -0,0 +1,15 @@ +#include "notification_badge.h" +#include + +int main(){ + std::vector original(128*128*4,40),pixels=original; + notification_badge(pixels,0);assert(pixels==original); + notification_badge(pixels,1);assert(pixels!=original); + auto one=pixels;pixels=original;notification_badge(pixels,12);assert(pixels!=one); + pixels=original;notification_badge(pixels,99);auto ninety_nine=pixels; + pixels=original;notification_badge(pixels,100);assert(pixels!=ninety_nine); + auto maximum=pixels;pixels=original;notification_badge(pixels,256);assert(pixels==maximum); + for(int y=0;y<128;y++)for(int x=0;x<128;x++)if(y>=33||x<74) + for(int c=0;c<4;c++)assert(pixels[(y*128+x)*4+c]==40); + std::vector invalid(12,0);notification_badge(invalid,1);assert(invalid==std::vector(12,0)); +} diff --git a/tests/notification_geometry.cpp b/tests/notification_geometry.cpp index 8da489b..89ab114 100644 --- a/tests/notification_geometry.cpp +++ b/tests/notification_geometry.cpp @@ -2,6 +2,47 @@ #include int main(){ + // A side-offset toast must face the eye, including tilted/rotated tracking frames. + for(float yaw:{-.7f,0.f,.8f})for(float tilt:{-.6f,0.f,.5f})for(float radius:{.6f,1.3f,2.f}){ + float cy=std::cos(yaw),sy=std::sin(yaw),ct=std::cos(tilt),st=std::sin(tilt); + vr::HmdMatrix34_t head{{{cy,sy*st,sy*ct,.6f},{0,ct,-st,.8f},{-sy,cy*st,cy*ct,-.7f}}}; + NotificationPlacement toast{};assert(notification_placement(head,radius,toast)); + auto& m=toast.transform; + vr::HmdVector3_t eye{},right{{m.m[0][0],m.m[1][0],m.m[2][0]}},up{{m.m[0][1],m.m[1][1],m.m[2][1]}}; + float distance=0; + for(int r=0;r<3;r++){ + eye.v[r]=head.m[r][3]-m.m[r][3];distance+=eye.v[r]*eye.v[r]; + float expected=head.m[r][3]+head.m[r][0]*.24f+head.m[r][1]*.12f-head.m[r][2]*1.f; + assert(std::abs(expected-m.m[r][3])<1e-6f); + } + assert(normalize_dock_vector(eye));auto normal=cross_dock_vector(right,up); + for(int r=0;r<3;r++){assert(std::abs(eye.v[r]-m.m[r][2])<1e-6f);assert(std::abs(normal.v[r]-m.m[r][2])<1e-6f);} + assert(toast.radius==radius&&toast.width==.32f); + // Both ends of the compact cylindrical surface remain front-facing to the eye. + for(float side:{-1.f,1.f}){ + float angle=side*toast.width*.5f/radius,dot=0; + for(int r=0;r<3;r++){ + float point=m.m[r][3]+right.v[r]*radius*std::sin(angle)+normal.v[r]*radius*(1-std::cos(angle)); + float front=normal.v[r]*std::cos(angle)-right.v[r]*std::sin(angle); + dot+=front*(head.m[r][3]-point); + } + assert(dot>0); + } + } + NotificationPlacement invalid{};vr::HmdMatrix34_t zero{};assert(!notification_placement(zero,1.3f,invalid)); + vr::HmdMatrix34_t identity{{{1,0,0,0},{0,1,0,0},{0,0,1,0}}}; + assert(!notification_placement(identity,0,invalid));assert(!notification_placement(identity,NAN,invalid)); + // A card painted at the top of the UI must hit at the top, not below it. + std::vector card(600*360*4,0); + for(int y=360-8-150;y<360-8;y++)for(int x=8;x<592;x++)card[(y*600+x)*4+3]=255; + auto top=notification_bounds(card,600,360,1); + assert(top.x==8&&top.y==8&&top.width==584&&top.height==150); + NotificationClick click;auto now=std::chrono::steady_clock::now();int x=110,y=106; + click.begin(100,100,now);assert(click.hold(x,y,now+std::chrono::milliseconds(80))); + assert(click.release(x,y,now+std::chrono::milliseconds(180)));assert(x==100&&y==100&&!click.active); + click.begin(100,100,now);assert(!click.hold(140,100,now));x=101;y=100;assert(!click.release(x,y,now));assert(x==101); + click.begin(100,100,now);x=y=100;assert(!click.release(x,y,now+std::chrono::seconds(1))); + click.begin(100,100,now);click.clear();assert(!click.release(x,y,now)); std::vector pixels(12*8*4,0); assert(!notification_bounds(pixels,12,8,2).visible()); // RGB left over in a transparent frame must not capture input. @@ -11,13 +52,13 @@ int main(){ pixels[(4*12+9)*4+3]=1; auto bounds=notification_bounds(pixels,12,8,2); assert(bounds.visible()); - assert(bounds.x==1&&bounds.y==.5f&&bounds.width==4&&bounds.height==2); + assert(bounds.x==1&&bounds.y==1.5f&&bounds.width==4&&bounds.height==2); // Dismissing/expiring the final toast yields an empty frame again. pixels[(1*12+2)*4+3]=0;pixels[(4*12+9)*4+3]=0; assert(!notification_bounds(pixels,12,8,2).visible()); pixels.back()=255; bounds=notification_bounds(pixels,12,8,2); - assert(bounds.x==5.5f&&bounds.y==3.5f&&bounds.width==.5f&&bounds.height==.5f); + assert(bounds.x==5.5f&&bounds.y==0&&bounds.width==.5f&&bounds.height==.5f); assert(!notification_bounds(pixels,12,8,0).visible()); assert(!notification_bounds({},12,8,2).visible()); } diff --git a/tests/notification_settings_fixture.tsx b/tests/notification_settings_fixture.tsx new file mode 100644 index 0000000..e5695dc --- /dev/null +++ b/tests/notification_settings_fixture.tsx @@ -0,0 +1,92 @@ +// Exercise the real notification controls, including failures and hidden headset views. +import React,{useState} from 'react'; +import {createRoot} from 'react-dom/client'; +import {NotificationSettings,PopupPermission,useReadInbox,type NotificationConfig} from '../ui/src/notification-settings'; +import {NotificationCenter,type NotificationEntry} from '../ui/src/notifications'; +import {configureLanguage} from '../ui/src/i18n'; +import '../ui/src/style.css'; +const root=createRoot(document.getElementById('root')!); +const wait=(ms=80)=>new Promise(r=>setTimeout(r,ms)); +const plugins={ + 'capture.clip':{manifest:{id:'capture.clip',name:'Clip capture',description:'Capture your session',version:'1.0.0'},enabled:true,order:0}, + 'capture.off':{manifest:{id:'capture.off',name:'Frame diagnostics',description:'Device diagnostics',version:'1.0.0'},enabled:false,order:1}, +}; +let config:NotificationConfig={popups:true,badge:true,plugins:{}},fail=false,visible=false,readCalls=0; +const messages:NotificationEntry[]=[{plugin:'capture.clip',pluginName:'Clip capture',createdAt:Date.now(),expiresAt:null,toast:false,inInbox:true,notification:{id:'saved',title:'录像已保存',body:'录像已经保存,可以稍后打开。',inbox:true}}]; +const calls:any[]=[]; +window.fetch=async(_url,init)=>{ + const{method,params}=JSON.parse(String(init?.body));calls.push({method,params});let result:any=true; + if(method==='status')result={version:'0.4.2-preview.12',agreement:{accepted:true,version:'fixture'},database:{plugins,sources:[],safeMode:false,language:'en-US',notificationSettings:config},notifications:[],inbox:messages,notificationBadge:0,running:[]}; + else if(method==='language.list')result={packs:[]}; + else if(method==='ui.events')result={events:[],cursor:0}; + else if(method==='notification.settings.save'){ + if(fail)return new Response(JSON.stringify({error:'Fixture settings failed'})); + config={...config,plugins:{...config.plugins}}; + const preference=typeof params.plugin==='string'?(config.plugins[params.plugin]={...(config.plugins[params.plugin]??{popups:true,badge:true})}):config; + for(const key of ['popups','badge'] as const)if(typeof params[key]==='boolean')preference[key]=params[key]; + }else if(method==='ui.visibility.get')result={known:true,pageVisible:visible}; + else if(method==='notification.read'){ + readCalls++; + for(const item of params.entries){const n=messages.find(n=>n.plugin===item.plugin&&n.notification.id===item.id&&n.createdAt===item.createdAt);if(n)n.read=true;} + }else if(!['host.manager.open','host.haptic'].includes(method))throw Error('Unexpected API '+method); + return new Response(JSON.stringify({result})); +}; +let switchMode:(value:'settings'|'quick')=>void,refresh:()=>Promise; +function Fixture(){ + const[mode,setMode]=useState<'settings'|'quick'>('settings'),[,update]=useState(0); + switchMode=setMode;refresh=async()=>update(v=>v+1); + useReadInbox(mode==='quick',messages,refresh); + return
{mode==='settings'?:}/>}
; +} +function control(label:string){const node=document.querySelector(`[role=switch][aria-label="${label}"]`);if(!node)throw Error('Missing switch '+label);return node as HTMLButtonElement;} +async function until(check:()=>boolean){for(let i=0;i<60;i++){if(check())return;await wait();}throw Error('UI timeout');} +(window as any).runInstallReviewChecks=async()=>{try{ + configureLanguage('zh-CN',[]);await refresh();await wait(150); + if(document.querySelectorAll('.notification-plugin-row').length!==3||document.querySelectorAll('[role=switch]').length!==8)throw Error('Installed plugins or notification controls missing'); + if(!document.body.textContent!.includes('已停用'))throw Error('Disabled plugin omitted'); + if(!document.body.textContent!.includes('本体通知,包括检查更新提醒。'))throw Error('Framely notification source missing'); + const globals=document.querySelector('.notification-global-options') as HTMLElement,list=document.querySelector('.notification-plugin-list') as HTMLElement; + if(Math.abs(globals.getBoundingClientRect().width-list.getBoundingClientRect().width)>1)throw Error('Global options and app list widths differ'); + const globalSwitch=control('悬浮通知').getBoundingClientRect(),popupSwitch=control('Framely:悬浮').getBoundingClientRect(); + if(Math.abs(globalSwitch.left-popupSwitch.left)>1)throw Error('Global and app popup switch columns are misaligned'); + control('Framely:悬浮').click();await until(()=>control('Framely:悬浮').getAttribute('aria-checked')==='false'); + control('Framely:角标').click();await until(()=>control('Framely:角标').getAttribute('aria-checked')==='false'); + if(!config.popups||!config.badge||config.plugins[''].popups||config.plugins[''].badge)throw Error('Core preferences changed global policy'); + if(!calls.some(c=>c.method==='notification.settings.save'&&c.params.plugin===''&&c.params.popups===false))throw Error('Framely popup preference was saved as global'); + console.log('FRAMELY_PREVIEW_INSTALL_NOTIFICATION_SETTINGS_ZH'); + fail=true;control('悬浮通知').click();await until(()=>!!document.querySelector('[role=alert]')); + if(!config.popups||control('悬浮通知').getAttribute('aria-checked')!=='true')throw Error('Failed save changed switch'); + fail=false;control('悬浮通知').click();await until(()=>control('悬浮通知').getAttribute('aria-checked')==='false'); + if(!config.badge)throw Error('Popup switch changed badge policy'); + control('Clip capture:角标').click();await until(()=>control('Clip capture:角标').getAttribute('aria-checked')==='false'); + if(config.plugins['capture.clip'].popups!==true)throw Error('Plugin badge switch changed popup policy'); + control('Frame diagnostics:悬浮').click();await until(()=>control('Frame diagnostics:悬浮').getAttribute('aria-checked')==='false'); + control('启动图标角标').click();await until(()=>control('启动图标角标').getAttribute('aria-checked')==='false'); + if(config.plugins['capture.clip'].badge!==false||config.plugins['capture.off'].popups!==false)throw Error('Global switch erased plugin preferences'); + const search=document.querySelector('[aria-label="搜索通知应用"]') as HTMLInputElement; + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')!.set!.call(search,'Frame diagnostics');search.dispatchEvent(new Event('input',{bubbles:true}));await wait(); + if(document.querySelectorAll('.notification-plugin-row').length!==1)throw Error('Plugin search failed'); + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value')!.set!.call(search,'');search.dispatchEvent(new Event('input',{bubbles:true}));await wait(); + configureLanguage('en-US',[]);await refresh();await wait(180); + if(!document.body.textContent!.includes('Launcher badge')||!document.body.textContent!.includes('App notifications'))throw Error('English settings copy missing'); + console.log('FRAMELY_PREVIEW_INSTALL_NOTIFICATION_SETTINGS_EN'); + for(const row of document.querySelectorAll('.notification-plugin-row'))if(row.scrollWidth>row.clientWidth)throw Error('Plugin switches overflow the settings row'); + switchMode('quick');configureLanguage('zh-CN',[]);await wait(180); + if(control('允许通知').getAttribute('aria-checked')!=='false'||!document.body.textContent!.includes('悬浮已关闭'))throw Error('Quick popup switch out of sync'); + if(readCalls)throw Error('Hidden headset view marked notifications as read'); + console.log('FRAMELY_PREVIEW_INSTALL_NOTIFICATION_PERMISSION_ZH'); + control('允许通知').click();await until(()=>control('允许通知').getAttribute('aria-checked')==='true'); + if(!config.popups||config.badge)throw Error('Quick switch changed unrelated setting'); + (document.querySelector('[aria-label="通知设置"]') as HTMLButtonElement).click();await wait(); + if(!calls.some(c=>c.method==='host.manager.open'&&c.params.page==='notification-settings'))throw Error('Notification settings entry target missing'); + visible=true;await until(()=>readCalls>0); + if(!messages[0].read||!document.querySelector('.inbox-message'))throw Error('Reading inbox removed message'); + root.unmount();history.replaceState(null,'','/manager#notification-settings');await import('../ui/src/main'); + await until(()=>!!document.querySelector('.notification-settings')); + const nav=[...document.querySelectorAll('nav button')].find(b=>b.textContent==='Notifications'); + if(!nav||nav.getAttribute('aria-current')!=='page')throw Error('Manager notification settings navigation missing'); + if(document.querySelectorAll('.notification-plugin-row').length!==3)throw Error('Manager notification settings omitted notification source'); + await wait(180);console.log('FRAMELY_PREVIEW_INSTALL_NOTIFICATION_MANAGER_EN'); + console.log('FRAMELY_BRIDGE_PASS'); +}catch(e){console.error('FRAMELY_BRIDGE_FAIL '+e);}}; +root.render();wait(150).then(()=>console.log('FRAMELY_VIEW_READY')); diff --git a/tests/notifications_fixture.tsx b/tests/notifications_fixture.tsx new file mode 100644 index 0000000..79632c8 --- /dev/null +++ b/tests/notifications_fixture.tsx @@ -0,0 +1,104 @@ +// Exercise the actual notification views and quick menu in CEF, including saved messages and expiry. +import React,{useState} from 'react'; +import {createRoot} from 'react-dom/client'; +import {NotificationCenter,type NotificationEntry} from '../ui/src/notifications'; +import {configureLanguage} from '../ui/src/i18n'; +import '../ui/src/style.css'; +const root=createRoot(document.getElementById('root')!); +const wait=(ms=80)=>new Promise(r=>setTimeout(r,ms)); +const plugin={manifest:{id:'capture.clip',name:'Clip capture',description:'Capture your session',version:'1.0.0',author:'Framely',ui:{windows:{}}},enabled:true,favorite:false,order:0}; +const plugins:Record={'capture.clip':plugin,'capture.off':{...plugin,enabled:false,order:1,manifest:{...plugin.manifest,id:'capture.off',name:'Disabled capture'}}}; +const entry=(id:string,extra:Partial={}):NotificationEntry=>({plugin:'capture.clip',pluginName:'Clip capture',createdAt:Date.now(),expiresAt:Date.now()+8000,toast:true,inInbox:true,notification:{id,title:'录像已保存',body:'录像已经保存,可以打开文件或继续录制。',inbox:true,actions:[{id:'open',label:'打开录像'}]},...extra}); +let entries=[entry('saved'),entry('transient',{inInbox:false,notification:{id:'transient',title:'临时消息',body:'只显示浮动通知'}}),entry('update',{plugin:'',pluginName:'Framely',update:{version:'0.4.3-preview.1'},notification:{id:'update',title:'Update',body:'New release',actions:[{id:'ignore',label:'Ignore'},{id:'open',label:'Open updates'}]}})]; +let language='zh-CN',fail=false,calls:any[]=[],render:(inbox:boolean)=>void; +const status=()=>({version:'0.4.2-preview.12',agreement:{accepted:true,version:'fixture'},database:{plugins,sources:[],safeMode:false,language},notifications:entries.filter(n=>n.toast),inbox:entries.filter(n=>n.inInbox),notificationsCount:entries.length,running:[]}); +window.fetch=async(_url,init)=>{ + const{method,params}=JSON.parse(String(init?.body));let result:any=true; + if(method==='status')result=status(); + else if(method==='language.list')result={packs:[]}; + else if(method==='language.save'){language=params.language;calls.push({method,params});} + else if(method==='host.manager.open'){calls.push({method,params});} + else if(method==='ui.events')result={events:[],cursor:0}; + else if(method==='ui.visibility.get')result={known:false,pageVisible:false}; + else if(method==='notification.read'){ + for(const item of params.entries){const n=entries.find(n=>n.plugin===item.plugin&&n.notification.id===item.id&&n.createdAt===item.createdAt);if(n)n.read=true;} + } + else if(method.startsWith('notification.')){ + calls.push({method,params}); + const n=entries.find(n=>n.plugin===params.plugin&&n.notification.id===params.id); + if(!n)throw Error('unknown notification'); + if(params.createdAt!==n.createdAt)throw Error('stale notification'); + if(fail)return new Response(JSON.stringify({error:'Fixture action failed'})); + if(method==='notification.action'){ + const a=n.notification.actions!.find(a=>a.id===params.action)!; + n.toast=a.closeOnClick===false?n.toast:false;n.inInbox=a.removeFromInboxOnClick===false?n.inInbox:false; + if(n.update&&a.id==='open')result={openUpdates:true}; + }else if(method==='notification.dismiss')n.toast=false; + else if(method==='notification.remove'){n.toast=false;n.inInbox=false;} + }else if(!['host.haptic','host.keyboard','host.manager.open','host.menu.close'].includes(method))throw Error('unexpected API '+method); + return new Response(JSON.stringify({result})); +}; +function Fixture(){const[value,setValue]=useState(entries),[inbox,setInbox]=useState(true);render=inbox=>{setValue([...entries]);setInbox(inbox);};return
setValue([...entries])}/>
;} +function button(label:string){const b=document.querySelector(`button[aria-label="${label}"]`);if(!b)throw Error('missing button '+label);return b as HTMLButtonElement;} +async function until(check:()=>boolean){for(let i=0;i<60;i++){if(check())return;await wait();}throw Error('UI timeout');} +(window as any).runInstallReviewChecks=async()=>{try{ + configureLanguage('zh-CN',[]);render(true);await wait(); + if(document.querySelectorAll('.inbox-message').length!==2||document.body.textContent!.includes('临时消息'))throw Error('non-inbox notification leaked into inbox'); + if(!document.body.textContent!.includes('录像已保存')||!document.body.textContent!.includes('打开更新'))throw Error('Chinese notification content missing'); + await wait(150);console.log('FRAMELY_PREVIEW_INSTALL_INBOX_ZH'); + fail=true;button('打开录像').click();await until(()=>!!document.querySelector('[role=alert]')); + if(!entries[0].inInbox||!entries[0].toast)throw Error('failed action removed message'); + fail=false;button('打开录像').click();await until(()=>document.querySelectorAll('.inbox-message').length===1); + if(entries[0].inInbox||entries[0].toast)throw Error('default action did not remove message'); + entries=[entry('keep',{notification:{id:'keep',title:'录制进行中',body:'查看状态不会关闭这条通知。',actions:[{id:'open',label:'查看状态',closeOnClick:false,removeFromInboxOnClick:false}]} })]; + render(false);await wait();button('查看状态').click();await wait(); + if(!entries[0].toast||!entries[0].inInbox||!document.querySelector('.toast'))throw Error('opt-out action dismissed message'); + if(!document.querySelector('[role=timer]')?.textContent?.includes('秒后关闭'))throw Error('countdown not shown'); + button('关闭通知').click();await until(()=>!document.querySelector('.toast')); + if(!entries[0].inInbox)throw Error('floating close removed saved message'); + render(true);await wait();button('移除通知').click();await until(()=>!!document.querySelector('.inbox-empty')); + entries=[entry('expiry',{expiresAt:Date.now()+350})];render(false);await wait(700); + if(document.querySelector('.toast')||!entries[0].inInbox)throw Error('automatic close removed inbox copy'); + entries=[entry('persistent',{expiresAt:null,notification:{id:'persistent',title:'Persistent status',body:'Stays visible until dismissed',durationMs:0}})];render(false);await wait(); + if(!document.querySelector('.toast')||document.querySelector('[role=timer]'))throw Error('persistent notification has a countdown'); + entries=[entry('long',{notification:{id:'long',title:'A recording with a long title is ready to review',body:'Recording details. '.repeat(80),actions:[{id:'open',label:'Open recording'},{id:'copy',label:'Copy path'},{id:'continue',label:'Continue recording'}]}})];render(false);await wait(); + const compact=document.querySelector('.toast') as HTMLElement,message=compact.querySelector('.notification-message') as HTMLElement; + if(compact.getBoundingClientRect().height>185||message.scrollHeight<=message.clientHeight)throw Error('long notification expanded instead of scrolling inside compact popup'); + for(const action of compact.querySelectorAll('.toast-actions button')){const bounds=action.getBoundingClientRect();if(bounds.height<44||bounds.bottom>compact.getBoundingClientRect().bottom)throw Error('compact notification action is clipped or too small');} + entries=[entry('saved',{notification:{id:'saved',title:'Recording saved',body:'Your recording is ready. Open it whenever you are ready.',actions:[{id:'open',label:'Open recording'}]}}),entry('update',{plugin:'',pluginName:'Framely',update:{version:'0.4.3-preview.1'},notification:{id:'update',title:'Update',body:'New release',actions:[{id:'ignore',label:'Ignore'},{id:'open',label:'Open updates'}]}})]; + configureLanguage('en-US',[]);render(true);await wait(180); + if(!document.body.textContent!.includes('Inbox')||!document.body.textContent!.includes('Open updates'))throw Error('English translation missing'); + console.log('FRAMELY_PREVIEW_INSTALL_INBOX_EN'); + render(false);await wait(180);console.log('FRAMELY_PREVIEW_INSTALL_TOAST_EN'); + if([...document.querySelectorAll('.toast')].some(card=>card.getBoundingClientRect().height>185))throw Error('floating notification exceeds compact height'); + if([...document.querySelectorAll('.toast-actions button')].some(b=>b.scrollWidth>b.clientWidth))throw Error('notification action overflowed'); + // Mount the real quick menu and assert the navigation actually reaches the inbox. + root.unmount();await import('../ui/src/main'); + await until(()=>!!document.querySelector('nav button')); + const installed=[...document.querySelectorAll('nav button')].find(b=>b.textContent==='已安装') as HTMLButtonElement; + installed.click();await wait(); + if(document.querySelectorAll('.plugin-row').length!==1)throw Error('quick installed list includes disabled plugins'); + const notifications=[...document.querySelectorAll('nav button')].find(b=>b.textContent?.startsWith('通知')) as HTMLButtonElement; + if(!notifications)throw Error('quick inbox tab missing');notifications.click();await wait(180); + if(document.querySelectorAll('.inbox-message').length!==2||document.querySelectorAll('nav button').length!==4)throw Error('quick inbox navigation broken'); + if(!document.querySelector('[role=switch][aria-label="允许通知"]')||!document.querySelector('[aria-label="通知设置"]'))throw Error('quick notification controls missing'); + console.log('FRAMELY_PREVIEW_INSTALL_QUICK_INBOX'); + button('打开更新').click();await until(()=>!document.querySelector('.inbox-message')?.textContent?.includes('可更新')); + if(!calls.some(c=>c.method==='notification.action'&&c.params.action==='open'&&c.params.plugin===''))throw Error('update action not dispatched'); + const settings=[...document.querySelectorAll('nav button')].find(b=>b.textContent==='设置') as HTMLButtonElement; + settings.click();await until(()=>!!document.querySelector('.quick-settings')); + if(document.querySelector('.quick-settings input[type=file]')||document.querySelector('.quick-settings [download]'))throw Error('Quick settings exposes language installation or template download'); + const safe=document.querySelector('.setting-toggle') as HTMLElement,languageCard=document.querySelector('.language-settings') as HTMLElement; + if(languageCard.getBoundingClientRect().top-safe.getBoundingClientRect().bottom<24)throw Error('Safe mode is too close to language divider'); + button('界面语言').click();await wait(); + const english=[...document.querySelectorAll('[role=option]')].find(b=>b.textContent==='English') as HTMLButtonElement; + if(!english)throw Error('Language picker has no English option');english.click(); + await until(()=>!!document.querySelector('[aria-label="Interface language"]')); + if(!calls.some(c=>c.method==='language.save'&&c.params.language==='en-US'))throw Error('Quick language selection did not save'); + const update=document.querySelector('.quick-update-entry') as HTMLButtonElement; + if(!update?.textContent?.includes('Check for updates'))throw Error('Quick update entry missing');update.click();await wait(); + if(!calls.some(c=>c.method==='host.manager.open'&&c.params.page==='updates'))throw Error('Quick update entry did not open update settings'); + console.log('FRAMELY_PREVIEW_INSTALL_QUICK_SETTINGS_EN'); + console.log('FRAMELY_BRIDGE_PASS'); +}catch(e){console.error('FRAMELY_BRIDGE_FAIL '+e);}}; +root.render();wait(150).then(()=>console.log('FRAMELY_VIEW_READY')); diff --git a/tests/plugin_navigation.cpp b/tests/plugin_navigation.cpp new file mode 100644 index 0000000..0d31fc4 --- /dev/null +++ b/tests/plugin_navigation.cpp @@ -0,0 +1,13 @@ +#include "plugin_navigation.h" +#include +int main() { + const std::string origin="http://localhost:19626"; + assert(allow_plugin_navigation(origin+"/window",origin,true,true)); + assert(allow_plugin_navigation("about:blank",origin,false,false)); + assert(allow_plugin_navigation("http://localhost:19629/framely-window/main",origin,false,true)); + assert(allow_plugin_navigation("http://localhost:19629/framely-window/login?ticket=test",origin,false,true)); + assert(!allow_plugin_navigation("http://localhost:19629/",origin,true,true)); + assert(!allow_plugin_navigation("http://localhost:19629/",origin,false,false)); + for (const auto& url : {"https://example.com/", "http://192.168.5.67:19629/", "http://localhost.evil:19629/", "http://localhost:19629@evil/", "http://localhost:19629\\@evil/", "http://localhost:0/", "http://localhost:65536/", "http://localhost:/", "http://localhost:999999999999999999999/"}) + assert(!allow_plugin_navigation(url,origin,false,true)); +} diff --git a/tests/system_updates_fixture.tsx b/tests/system_updates_fixture.tsx index 1f31e00..74f60b5 100644 --- a/tests/system_updates_fixture.tsx +++ b/tests/system_updates_fixture.tsx @@ -10,12 +10,13 @@ const wait=(ms=70)=>new Promise(resolve=>setTimeout(resolve,ms)); let status:any={version:'0.4.3-preview.1',build:'0.4.3-preview.1-012345abcdef',database:{updateSource:{url:'https://github.com/example/framely/releases/latest/download/framely-release.json'}},previousRelease:'releases/old'}; let updateStatus:React.Dispatch>,remount:()=>void; let kind='',empty=false,hold=false,verifying=false,failCheck=false,failDownload=false,failApply=false; -let checks=0,downloads=0,attempts=0,installs=0,rollbacks=0; +let checks=0,downloads=0,attempts=0,installs=0,rollbacks=0,automaticSaves=0; const release=()=>({version:(status.database.updateChannel==='testing'?'0.4.3-preview.2':'0.4.2')+'-abcdef012345',size:1024*1024,changelog:'Fixture release notes'}); window.fetch=async(_url,init)=>{ const {method,params}=JSON.parse(String(init?.body)); let result:any=true; - if(method==='system.channel.save')status={...status,database:{...status.database,updateChannel:params.channel}}; + if(method==='system.check.settings.save'){automaticSaves++;status={...status,database:{...status.database,updateCheck:params.settings}};} + else if(method==='system.channel.save')status={...status,database:{...status.database,updateChannel:params.channel}}; else if(method==='system.check.start'){kind='system.check';checks++;result={job:'fixture'};} else if(method==='system.download.start'){kind='system.download';downloads++;result={job:'fixture'};} else if(method==='system.job.status'){ @@ -42,6 +43,14 @@ async function choose(label:string){(document.querySelector('.framely-select but try{ configureLanguage('zh-CN',[]);updateStatus({...status});await wait(); if(!document.querySelector('.framely-select button')?.textContent?.includes('正式版'))throw Error('legacy source did not default to stable'); + const automaticSwitch=()=>document.querySelector('.automatic-update-settings input[type=checkbox]') as HTMLInputElement; + if(!automaticSwitch().checked)throw Error('automatic checks did not default to enabled'); + automaticSwitch().click();await until(()=>automaticSaves===1);await wait(); + if(status.database.updateCheck.enabled||(document.querySelector('.automatic-update-settings .framely-select button') as HTMLButtonElement).disabled===false)throw Error('disabled automatic check settings not reflected'); + automaticSwitch().click();await until(()=>automaticSaves===2);await wait(); + (document.querySelector('.automatic-update-settings .framely-select button') as HTMLButtonElement).click();await wait(); + ([...document.querySelectorAll('[role=option]')].find(o=>o.textContent==='每 12 小时') as HTMLButtonElement).click();await until(()=>automaticSaves===3);await wait(); + if(status.database.updateCheck.intervalHours!==12)throw Error('automatic interval not saved'); button('检查更新').click();await until(()=>!!document.querySelector('.release-card')); if(!document.querySelector('.release-card')?.textContent?.includes('较旧版本'))throw Error('channel downgrade not identified'); button('下载并安装').focus();button('下载并安装').click();await wait(); @@ -72,7 +81,7 @@ async function choose(label:string){(document.querySelector('.framely-select but if(document.querySelector('.release-card')?.textContent?.includes('较旧版本'))throw Error('new preview reported as downgrade'); if([...document.querySelectorAll('button')].some(b=>b.textContent==='安装已校验版本'))throw Error('download reused across channels'); hold=true;failApply=false;button('下载并安装').click();await wait();button('确认下载并安装').click();await until(()=>downloads===3);await wait(); - if(!(document.querySelector('.framely-select button') as HTMLButtonElement).disabled||!button('处理中…').disabled)throw Error('settings mutable during download'); + if(!(document.querySelector('.framely-select button') as HTMLButtonElement).disabled||!automaticSwitch().disabled||!button('处理中…').disabled)throw Error('settings mutable during download'); if(attempts!==2)throw Error('installation started before verification'); await until(()=>!!document.querySelector('progress[aria-label="下载进度"]')); verifying=true; diff --git a/tools/package-release.sh b/tools/package-release.sh index 9990fb4..654e2b9 100755 --- a/tools/package-release.sh +++ b/tools/package-release.sh @@ -26,6 +26,7 @@ cp native/vendor/openvr/lib/linuxarm64/libopenvr_api.so "$stage/lib/openvr/" cp -a ui/dist/. "$stage/share/ui/" cp "$cef/LICENSE.txt" "$stage/share/licenses/cef.txt" cp native/vendor/openvr/LICENSE "$stage/share/licenses/openvr.txt" +cp node_modules/@tabler/icons-react/LICENSE "$stage/share/licenses/tabler-icons.txt" [[ ! -f $cef/CREDITS.html ]] || cp "$cef/CREDITS.html" "$stage/share/licenses/cef-credits.html" cp native/vendor/json.hpp "$stage/share/licenses/nlohmann-json.hpp" cp packaging/*.sh "$stage/" diff --git a/tools/plugin-dev.mjs b/tools/plugin-dev.mjs index bfd155d..701b5e3 100644 --- a/tools/plugin-dev.mjs +++ b/tools/plugin-dev.mjs @@ -28,7 +28,7 @@ if(command==='init'){ const ctx=await context(options);await ctx.watch(); const server=http.createServer(async(req,res)=>{try{ if(req.url==='/__framely/events'){res.writeHead(200,{'Content-Type':'text/event-stream','Cache-Control':'no-cache'});res.write('data: connected\n\n');clients.add(res);req.on('close',()=>clients.delete(res));return;} - if(req.url==='/'||req.url==='/quick'||req.url?.startsWith('/window/')||req.url?.startsWith('/?')){res.setHeader('Content-Type','text/html');res.end(`
`);return;} + if(req.url==='/'||req.url==='/quick'||req.url?.startsWith('/window/')||req.url?.startsWith('/?')){res.setHeader('Content-Type','text/html');res.end(`
`);return;} const name=req.url?.split('?')[0];if(name!=='/page.js'&&name!=='/page.js.map'){res.writeHead(404);res.end();return;}res.setHeader('Content-Type',name.endsWith('.map')?'application/json':'application/javascript');res.end(await readFile(path.join(root,'payload',name.slice(1)))); }catch(e){res.writeHead(500);res.end(String(e));}}); server.listen(Number(process.env.FRAMELY_DEV_PORT??5173),'127.0.0.1',()=>console.log(`Preview: http://127.0.0.1:${server.address().port} (no device privileges)`)); diff --git a/tools/test-install-review.sh b/tools/test-install-review.sh index 37c8dc9..a6b3142 100755 --- a/tools/test-install-review.sh +++ b/tools/test-install-review.sh @@ -16,6 +16,8 @@ const out=process.env.FRAMELY_INSTALL_REVIEW_OUTPUT; await build({entryPoints:[process.env.FRAMELY_INSTALL_REVIEW_FIXTURE],bundle:true,outfile:`${out}/fixture.js`,define:{'process.env.NODE_ENV':'"production"'}}); await writeFile(`${out}/index.html`,'
'); JS +mkdir -p "$out/assets" +cp -a assets/branding "$out/assets/branding" python3 - "$out" > "$out/server.log" 2>&1 <<'PY' & import functools,http.server,pathlib,sys root=pathlib.Path(sys.argv[1]) diff --git a/tools/test-native.sh b/tools/test-native.sh index a64b9b3..4049617 100755 --- a/tools/test-native.sh +++ b/tools/test-native.sh @@ -3,7 +3,7 @@ set -euo pipefail base=$(cd -- "$(dirname -- "$0")/.." && pwd) cd "$base" mkdir -p target/tests -for test in menu_geometry keyboard_state notification_geometry ui_visibility; do +for test in menu_geometry keyboard_state notification_geometry notification_badge ui_visibility plugin_navigation; do "${CXX:-g++}" -std=c++17 -O2 -Inative -Inative/vendor/openvr "tests/$test.cpp" -o "target/tests/$test" "target/tests/$test" done diff --git a/ui/locales/en-US.json b/ui/locales/en-US.json index 6aec176..866ce33 100644 --- a/ui/locales/en-US.json +++ b/ui/locales/en-US.json @@ -386,6 +386,53 @@ "仅检查正式版。": "Check stable releases only.", "仅检查测试版(Preview、Beta、RC 等)。": "Check testing releases only (Preview, Beta, RC, etc.).", "所选渠道暂无可用发行版本。": "No release is available in the selected channel.", - "所选版本早于当前版本,安装将切换到较旧版本。": "The selected release is older than your current version. Installing will switch to that older version." + "所选版本早于当前版本,安装将切换到较旧版本。": "The selected release is older than your current version. Installing will switch to that older version.", + "通知": "Notifications", + "收件箱": "Inbox", + "收件箱为空": "Your inbox is empty", + "需要保留的通知会出现在这里。": "Notifications saved for later will appear here.", + "移除通知": "Remove notification", + "{0} 秒后关闭": "Closes in {0}s", + "启用此插件后可使用通知操作。": "Enable this plugin to use notification actions.", + "Framely {0} 可更新": "Framely {0} is available", + "查看新版本并选择安装,或忽略本次提醒。": "Review and install the new version, or ignore this reminder.", + "忽略": "Ignore", + "打开更新": "Open updates", + "自动检查更新": "Check for updates automatically", + "检查间隔": "Check interval", + "每 {0} 小时": "Every {0} hours", + "发现新版本时发送通知,您可以忽略提醒或打开更新页面。": "New versions send a notification. Ignore it or open Updates when you are ready.", + "正在自动检查更新…": "Checking for updates automatically…", + "自动检查未完成,将在下次检查时重试。": "Automatic check failed. It will retry at the next interval.", + "Notification has changed": "This notification has changed. Please try again.", + "Notification inbox full": "The notification inbox is full. Remove older notifications first.", + "自动检查间隔须为 1-168 小时": "Automatic check interval must be 1-168 hours.", + "Update source or channel has changed": "The update source or channel has changed.", + "Notification queue full": "The notification popup queue is full.", + "每小时": "Every hour", + "允许通知": "Allow notifications", + "允许在视野中显示悬浮通知。": "Show floating notifications in your view.", + "悬浮已关闭,新通知仅在此处查看。": "Popups are off. View new notifications here.", + "通知设置": "Notification settings", + "选择通知如何提醒你。": "Choose how notifications reach you.", + "悬浮通知": "Floating notifications", + "关闭后,新通知将保存在通知收件箱,不显示浮窗。": "When off, new notifications go to your inbox without showing a popup.", + "启动图标角标": "Launcher badge", + "在 Framely 启动图标上显示未读收件箱消息数。": "Show the unread inbox count on the Framely launcher icon.", + "插件通知": "Plugin notifications", + "搜索通知插件": "Search notification plugins", + "全局开关优先;关闭全局开关不会改变插件偏好。": "Global switches take priority. Plugin preferences are kept when a global switch is off.", + "角标": "Badge", + "悬浮": "Popup", + "{0}:角标": "{0}: badge", + "{0}:悬浮": "{0}: popup", + "通知设置导航": "Notifications", + "应用通知": "App notifications", + "搜索通知应用": "Search notification apps", + "搜索应用": "Search apps", + "全局开关优先;关闭全局开关不会改变应用偏好。": "Global switches take priority. App preferences are kept when a global switch is off.", + "应用": "App", + "未找到应用": "No apps found", + "本体通知,包括检查更新提醒。": "Framely notifications, including update reminders." } } diff --git a/ui/locales/zh-CN.json b/ui/locales/zh-CN.json index ad3935a..2449bde 100644 --- a/ui/locales/zh-CN.json +++ b/ui/locales/zh-CN.json @@ -386,6 +386,53 @@ "仅检查正式版。": "仅检查正式版。", "仅检查测试版(Preview、Beta、RC 等)。": "仅检查测试版(Preview、Beta、RC 等)。", "所选渠道暂无可用发行版本。": "所选渠道暂无可用发行版本。", - "所选版本早于当前版本,安装将切换到较旧版本。": "所选版本早于当前版本,安装将切换到较旧版本。" + "所选版本早于当前版本,安装将切换到较旧版本。": "所选版本早于当前版本,安装将切换到较旧版本。", + "通知": "通知", + "收件箱": "收件箱", + "收件箱为空": "收件箱为空", + "需要保留的通知会出现在这里。": "需要保留的通知会出现在这里。", + "移除通知": "移除通知", + "{0} 秒后关闭": "{0} 秒后关闭", + "启用此插件后可使用通知操作。": "启用此插件后可使用通知操作。", + "Framely {0} 可更新": "Framely {0} 可更新", + "查看新版本并选择安装,或忽略本次提醒。": "查看新版本并选择安装,或忽略本次提醒。", + "忽略": "忽略", + "打开更新": "打开更新", + "自动检查更新": "自动检查更新", + "检查间隔": "检查间隔", + "每 {0} 小时": "每 {0} 小时", + "发现新版本时发送通知,您可以忽略提醒或打开更新页面。": "发现新版本时发送通知,您可以忽略提醒或打开更新页面。", + "正在自动检查更新…": "正在自动检查更新…", + "自动检查未完成,将在下次检查时重试。": "自动检查未完成,将在下次检查时重试。", + "Notification has changed": "通知已更新,请重试。", + "Notification inbox full": "通知收件箱已满,请先移除旧通知。", + "自动检查间隔须为 1-168 小时": "自动检查间隔须为 1-168 小时", + "Update source or channel has changed": "更新源或渠道已变更。", + "Notification queue full": "浮动通知队列已满。", + "每小时": "每小时", + "允许通知": "允许通知", + "允许在视野中显示悬浮通知。": "允许在视野中显示悬浮通知。", + "悬浮已关闭,新通知仅在此处查看。": "悬浮已关闭,新通知仅在此处查看。", + "通知设置": "通知设置", + "选择通知如何提醒你。": "选择通知如何提醒你。", + "悬浮通知": "悬浮通知", + "关闭后,新通知将保存在通知收件箱,不显示浮窗。": "关闭后,新通知将保存在通知收件箱,不显示浮窗。", + "启动图标角标": "启动图标角标", + "在 Framely 启动图标上显示未读收件箱消息数。": "在 Framely 启动图标上显示未读收件箱消息数。", + "插件通知": "插件通知", + "搜索通知插件": "搜索通知插件", + "全局开关优先;关闭全局开关不会改变插件偏好。": "全局开关优先;关闭全局开关不会改变插件偏好。", + "角标": "角标", + "悬浮": "悬浮", + "{0}:角标": "{0}:角标", + "{0}:悬浮": "{0}:悬浮", + "通知设置导航": "通知设置", + "应用通知": "应用通知", + "搜索通知应用": "搜索通知应用", + "搜索应用": "搜索应用", + "全局开关优先;关闭全局开关不会改变应用偏好。": "全局开关优先;关闭全局开关不会改变应用偏好。", + "应用": "应用", + "未找到应用": "未找到应用", + "本体通知,包括检查更新提醒。": "本体通知,包括检查更新提醒。" } } diff --git a/ui/src/language.tsx b/ui/src/language.tsx index 4e2e380..24de55e 100644 --- a/ui/src/language.tsx +++ b/ui/src/language.tsx @@ -2,12 +2,12 @@ import React,{useState} from 'react'; import {api} from './api'; import {Select} from './localized-select'; import {t,languages,resolveLanguage,LanguagePack} from './i18n'; -export function LanguageSettings({value,packs,invalidFiles,refresh}:{value:string;packs:LanguagePack[];invalidFiles:string[];refresh:()=>Promise}){ +export function LanguageSettings({value,packs,invalidFiles,refresh,selectionOnly=false}:{value:string;packs:LanguagePack[];invalidFiles:string[];refresh:()=>Promise;selectionOnly?:boolean}){ const[busy,setBusy]=useState(false),[error,setError]=useState(''); async function select(language:string){setBusy(true);setError('');try{await api('language.save',{language});await refresh();}catch(e){setError((e as Error).message);}finally{setBusy(false);}} async function install(file:File){setBusy(true);setError('');try{if(file.size>1024*1024)throw new Error(t('语言文件超过 1 MiB'));const pack=JSON.parse(await file.text());await api('language.install',{pack});await refresh();}catch(e){setError((e as Error).message);}finally{setBusy(false);}} - return

{t('语言')}

{invalidFiles.length>0&&

{t('无法读取语言文件:{0}',{0:invalidFiles.join(', ')})}

}{error&&

{error}

}
; + return

{t('语言')}

{!selectionOnly&&<>{t('下载语言文件模板')}}
{invalidFiles.length>0&&

{t('无法读取语言文件:{0}',{0:invalidFiles.join(', ')})}

}{error&&

{error}

}
; } export function LanguagePicker({value,packs,disabled,onChange}:{value:string;packs:LanguagePack[];disabled?:boolean;onChange:(language:string)=>void}){ diff --git a/ui/src/main.tsx b/ui/src/main.tsx index ba632b2..777baf7 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -1,3 +1,6 @@ +import {NotificationSettings,PopupPermission,useReadInbox,defaultNotificationConfig,type NotificationConfig} from './notification-settings'; +import {NotificationCenter,type NotificationEntry} from './notifications'; +import {IconBell} from '@tabler/icons-react'; import {t} from './i18n'; import {uploadFile} from './upload'; import {LanguageSettings} from './language'; @@ -15,7 +18,7 @@ import React,{useEffect,useRef,useState} from 'react';import{createRoot}from'rea type Manifest=Presentation&{id:string;name:string;version:string;author:string;description:string;backend?:{runAs:string;autostart:boolean;memoryLimitMiB?:number};lifecycle?:{runAs?:string};dependencies?:Record;optionalDependencies?:Record;conflicts?:Record;exclusiveResources?:string[];ui:{quickPage?:string;windows:Record}}; type Plugin={manifest:Manifest;enabled:boolean;favorite:boolean;order:number;source?:string;error?:string}; type Source={id:string;name:string;url:string;enabled:boolean;allowHttp:boolean}; -type Status={agreement?:AgreementState;version:string;build?:string;systemUpdate?:any;previousRelease?:string;database:{language?:string;proxy?:ProxyConfig;networkPanel?:NetworkPanel;plugins:Record;sources:Source[];subscriptions?:Subscription[];sourceOrigins?:Record}>;safeMode:boolean;updateSource?:{url:string}|null;updateChannel?:'stable'|'testing'};notifications:any[];runtime?:Record;running:string[]}; +type Status={agreement?:AgreementState;version:string;build?:string;systemUpdate?:any;previousRelease?:string;database:{notificationSettings?:NotificationConfig;language?:string;proxy?:ProxyConfig;networkPanel?:NetworkPanel;plugins:Record;sources:Source[];subscriptions?:Subscription[];sourceOrigins?:Record}>;safeMode:boolean;updateSource?:{url:string}|null;updateChannel?:'stable'|'testing'};notifications:NotificationEntry[];inbox?:NotificationEntry[];notificationBadge?:number;runtime?:Record;running:string[]}; const managerMode=location.pathname==='/manager'; const empty:Status={version:'0.4.0',database:{plugins:{},sources:[],safeMode:false},notifications:[],running:[]}; @@ -26,7 +29,7 @@ function PluginFrame({plugin,entry='quick'}:{plugin:string;entry?:string}){ const [attempt,setAttempt]=useState(0); useEffect(()=>{ let live=true;setError('');setSource(null); - const methods:Record={call:'plugin.call',dependencies:'plugin.dependencies','window.open':'window.open','window.close':'window.close','notification.send':'notification.send','notification.remove':'notification.remove'}; + const methods:Record={call:'plugin.call',dependencies:'plugin.dependencies','window.open':'window.open','window.close':'window.close','notification.send':'notification.send','notification.remove':'notification.remove','notification.dismiss':'notification.dismiss'}; async function message(e:MessageEvent){ if(e.source!==ref.current?.contentWindow||e.data?.channel!=='framely.plugin')return; const {id,op,params}=e.data;if(!Number.isSafeInteger(id)||typeof op!=='string')return; @@ -66,9 +69,10 @@ function PluginFrame({plugin,entry='quick'}:{plugin:string;entry?:string}){ return error?

{error}

:source?