From cdbe974e138e9acd91e381578ec97b13226f16a1 Mon Sep 17 00:00:00 2001 From: toorux Date: Sun, 4 Oct 2026 21:07:49 +0800 Subject: [PATCH] Apply unified branding and configurable plugin memory limits Prepare Framely 0.4.2-preview.6 and Installer 0.4.1-preview.4 with shared SVG artwork, platform icons, release packaging, and bilingual documentation. --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.en.md | 5 + README.md | 5 + assets/branding/README.md | 18 ++ assets/branding/framely-app-icon.png | Bin 0 -> 7043 bytes assets/branding/framely-app-icon.svg | 8 + assets/branding/framely-logo-light.svg | 8 + assets/branding/framely-logo.svg | 8 + assets/branding/framely-mark-light.svg | 7 + assets/branding/framely-mark.svg | 7 + assets/branding/framely.ico | Bin 0 -> 6276 bytes assets/branding/icons/1024.png | Bin 0 -> 16410 bytes assets/branding/icons/128.png | Bin 0 -> 1296 bytes assets/branding/icons/16.png | Bin 0 -> 184 bytes assets/branding/icons/24.png | Bin 0 -> 270 bytes assets/branding/icons/256.png | Bin 0 -> 3191 bytes assets/branding/icons/32.png | Bin 0 -> 327 bytes assets/branding/icons/48.png | Bin 0 -> 408 bytes assets/branding/icons/512.png | Bin 0 -> 7043 bytes assets/branding/icons/64.png | Bin 0 -> 482 bytes docs/developer-guide/manifest.md | 9 + docs/en/developer-guide/manifest.md | 9 + docs/en/releases.md | 12 +- docs/en/user-guide/installation.md | 2 + docs/releases.md | 12 +- docs/user-guide/installation.md | 2 + installer/Cargo.lock | 13 +- installer/Cargo.toml | 5 +- installer/README.en.md | 4 +- installer/README.md | 4 +- installer/build.rs | 11 ++ installer/install-desktop-entry.sh | 36 ++++ installer/src/main.rs | 2 + installer/src/ui.rs | 24 +-- native/brand_mask.h | 54 ++++++ native/host.cpp | 12 +- package-lock.json | 239 ++++++++++++++++++++++++- package.json | 32 +++- src/main.rs | 2 +- src/model.rs | 23 +++ src/process.rs | 3 +- src/session.rs | 63 ++++++- src/tests.rs | 63 ++++++- tests/install_review_fixture.tsx | 4 +- tools/build-branding.mjs | 46 +++++ tools/build-ui.mjs | 1 + tools/package-installer.py | 20 ++- tools/package-release.sh | 8 +- ui/index.html | 2 +- ui/locales/en-US.json | 1 + ui/locales/zh-CN.json | 1 + ui/src/agreement.tsx | 2 +- ui/src/icons.tsx | 2 +- ui/src/install-review.tsx | 2 + ui/src/main.tsx | 7 +- ui/src/memory-limit.tsx | 6 + ui/src/relations.tsx | 3 +- ui/src/style.css | 2 +- 59 files changed, 739 insertions(+), 74 deletions(-) create mode 100644 assets/branding/README.md create mode 100644 assets/branding/framely-app-icon.png create mode 100644 assets/branding/framely-app-icon.svg create mode 100644 assets/branding/framely-logo-light.svg create mode 100644 assets/branding/framely-logo.svg create mode 100644 assets/branding/framely-mark-light.svg create mode 100644 assets/branding/framely-mark.svg create mode 100644 assets/branding/framely.ico create mode 100644 assets/branding/icons/1024.png create mode 100644 assets/branding/icons/128.png create mode 100644 assets/branding/icons/16.png create mode 100644 assets/branding/icons/24.png create mode 100644 assets/branding/icons/256.png create mode 100644 assets/branding/icons/32.png create mode 100644 assets/branding/icons/48.png create mode 100644 assets/branding/icons/512.png create mode 100644 assets/branding/icons/64.png create mode 100644 installer/build.rs create mode 100644 installer/install-desktop-entry.sh create mode 100644 native/brand_mask.h create mode 100644 tools/build-branding.mjs create mode 100644 ui/src/memory-limit.tsx diff --git a/Cargo.lock b/Cargo.lock index 273eafe..027d53e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,7 +291,7 @@ dependencies = [ [[package]] name = "framely" -version = "0.4.2-preview.5" +version = "0.4.2-preview.6" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 4ae685d..919cd05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "framely" -version = "0.4.2-preview.5" +version = "0.4.2-preview.6" edition = "2021" license = "AGPL-3.0-only" diff --git a/README.en.md b/README.en.md index 1b3be4c..ab7dc26 100644 --- a/README.en.md +++ b/README.en.md @@ -1,5 +1,10 @@ # Framely + + + Framely + + [简体中文](README.md) 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. diff --git a/README.md b/README.md index f532288..eeb9b31 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Framely + + + Framely + + [English](README.en.md) Steam Frame 的 React 插件管理器:Rust 核心服务、独立 CEF/OpenVR 宿主、网络管理面板和电脑端安装器。可管理插件、来源、窗口、通知和更新。 diff --git a/assets/branding/README.md b/assets/branding/README.md new file mode 100644 index 0000000..6cae519 --- /dev/null +++ b/assets/branding/README.md @@ -0,0 +1,18 @@ +# Framely 品牌资源 / Branding assets + +像素窗口小精灵是本体和安装器的统一标识。`framely-mark.svg` 和 `framely-logo.svg` 为透明底深色版本,`*-light.svg` 用于深色背景;`framely-app-icon.svg` 为浅蓝底应用图标。字标已转成路径,无需安装字体。 + +The pixel window sprite is the shared identity for Framely and its installer. `framely-mark.svg` and `framely-logo.svg` are dark artwork on transparent backgrounds; `*-light.svg` variants are for dark surfaces. `framely-app-icon.svg` is the application tile on sky blue. The wordmark uses vector paths and needs no font installation. + +修改 SVG 后,在仓库根目录重新生成 PNG、Windows ICO 和原生入口图形,并一同提交生成文件: + +After editing the SVGs, regenerate PNGs, the Windows ICO and native Dock artwork from the repository root, then commit the generated files together: + +```bash +npm ci +npm run branding:build +``` + +macOS 打包时使用系统 `iconutil` 从 PNG 自动生成 `.icns`。运行时无需 Node.js;品牌资源采用仓库的 AGPL-3.0-only 许可证。 + +macOS packaging uses the system `iconutil` to generate `.icns` from the PNGs. Runtime does not require Node.js. These brand assets use the repository's AGPL-3.0-only license. diff --git a/assets/branding/framely-app-icon.png b/assets/branding/framely-app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5b5484b8aa91b65801f5b8b0375c8598c334c7c3 GIT binary patch literal 7043 zcmeHMX;f3!7TyUFgd&4l5e0%$^`S^q5D;amQ0f2?;VGb)s0iW^ka${-Yw07=xU2m-bt48;&EAdeZD0tvbIUFqw3!;iPtTdP0XRes&Q*52oweZTXa zv-kSWR|lLOm#C~#0e~fYzO+9GK*UWVELOxnli|%$fYp2V*xR{%>aj>2tF~|HBWL z4+wq5k55}i4_mfIjaD^!@G{-g%uLIdfs5-A{JMC zz9_LIlL5tnGI>M8vfctRTEC2K)wCtRn9%h?CMir3p{;_AN7J|@Bq3+_Ou@l&e_mkk zQ(8S;fJrRa;aE2dT^v)MzT>=eCY84R!`SFnpk+?o153}r4~UD zFr&bq{V7Ra&*O~0KGLh&xEyp)(0Jytn?D3yV@e3UyhH0fYDqr?77F18}VI)Qo`6g~gen#?Sk{9_AGWSc& z*`9ndgFRl%TRZax0=oUlJXf={2aCWx$!T_Cs*l2TbG~o}EM`l&vTtn06u{_*-PYu; zi-fz}0c?U2N*>x*=>|x)<&+m8t$*6eoFQTsB#d|9n`zYMNB(rK1`hCK*wBPXoQu8h z@81XXCpnkV#!YzY-jOq#@iCjZG9@G=;tiF+B?B5N>71l8CF-zgtM0T|X2|#9FdJe} z;O}MjJ-f*L6wrXd5wRru=tkgkqWgjfW`g1GU(sX}xhw*`EF&^7>fKmBLQ_IkSB!A2 zMe;}v0X9uGXeqFCVQz*DhZsCwgj}2vaXiuur{CLftW#LBb2bVC{@1#!C1Xv{Rf9qN zGkKW8TXi6d)6rK=Do`Bif0P&aK`0}1jj%Wh&@$SiPC7q_k|8jK$oH9uk&JLT;$$(E zrU2UKDk$f2$XFC+1KuU=T#H3O0TSZPs`$`~fqt@6I~F2UM#2DWjb&kr0KRktK!VM( z-)aKruOR>|UAkcWmLRzQ4U#Jb5H}D zibI~q3|?x&-`5VrUle?|3-N4a_w=>wtv}miK?E;*-E9G>NlFI>rEzm_J^dnLbsXXD z^}OAR&Vq3`@Cv5?i|CbH{YMy5F<{bR4 zDex$r%_`KMa40a@Zk7QHjz2mi#Q`p9J4*5{uOC15g_p_u>z=-6&g~-5-({HvdAle% z_0-(p_`KFCYiw={C-R>iTxuG!MAEeaX^@z(0SyB1op|;X5Ek~kV zsh-QAQDCfxiP|rX!9qd{2>ovkttri`$)~(r_D^oE+IIb?G-=?jUn#cW#vA;6)&GD+ zTYo)){t+P((AEB6mV?G$^z~wg?>$y7W6>YpE7HRh%3UisNTt+Vbq!({^R}w)>5{dX%#728h48`=r(HG7#e9 zaj95Y85WG+3FKX@M@p^QSgxna4mt6elVamktM~-z^7YGLb(h5EbF`6*K(z6S{0X2WUBeWSW8v)S37vzG@}#iwHm$so9{JdMF$67}a5tf)Wbv zHm>fTEiq(s(Yfczbxg4`bS?pS5R3^i!M$Z-6%fWA2^&o~$l4_WBxL6A(!}e@b>`6& z2;Zs?59`fob75n+AUkUa@bHC%D=X4l33JQw#oyV9<%iW(K{)A(bO=PHf+gUdqnDfsOEya?)gW-+Hxbfxh{TeC9>Ly z|G;hQbTzDAAUDv}1#}Xv*@v|aGpOopQ{3lnEq@XkHJjYz_1N`hKrrN?;e(yMUhN{I z&jIYm`^(Q2p}0yfjg`9iEJEt~6u@?idIS&i^HD{y{J?V z6{aZIhG4A(f2w-TdVm+R!O}-qS*6W1mh(~t615bG`~#)5F^PVhF5H*-Yeq*!&I|=B zfOKX|Nf3s<R5Nod>|qY!E(fbn=-kL!2e3KX)2t&|Z>k*) z+O7`5S2WGJKncfYohrl~cSYx#c?=q}jW)IO0shd^g;tS{W$IafM4~&|y}aWvx$m!N z+>hHq=PmXcY29mRgFjmEFEhW}ENg9)cA`73k`M(u0KL;~PuN>bK_|WXK=DvvL@YqQ z6R(?cN)|e(HKTjdMAYcSeAJ~yiw-$|xNe`ob=HG`2s?OHn|lj3WJdoytVbEy*;1*9 z_}@R?RZ6KBmWaJJpW;ZK+TlET)*o7WuG~CR+8Q{qOI_}-vJCWkdb~)o@yX>X#{PBa zICp;>^_+3yJO{g~nQnEcbIQ5Bv)(kbiri-`P9imu`!#k!PNoa1f}}d4cKpVKJr2(H Jg`d+e{R4vuOA-J8 literal 0 HcmV?d00001 diff --git a/assets/branding/framely-app-icon.svg b/assets/branding/framely-app-icon.svg new file mode 100644 index 0000000..0c070aa --- /dev/null +++ b/assets/branding/framely-app-icon.svg @@ -0,0 +1,8 @@ + + Framely + Charcoal pixel window sprite on a sky blue application tile. + + + + + diff --git a/assets/branding/framely-logo-light.svg b/assets/branding/framely-logo-light.svg new file mode 100644 index 0000000..f38b9f5 --- /dev/null +++ b/assets/branding/framely-logo-light.svg @@ -0,0 +1,8 @@ + + Framely + Pixel window sprite with the framely wordmark drawn as vector pixels. + + + + + diff --git a/assets/branding/framely-logo.svg b/assets/branding/framely-logo.svg new file mode 100644 index 0000000..cec439a --- /dev/null +++ b/assets/branding/framely-logo.svg @@ -0,0 +1,8 @@ + + Framely + Pixel window sprite with the framely wordmark drawn as vector pixels. + + + + + diff --git a/assets/branding/framely-mark-light.svg b/assets/branding/framely-mark-light.svg new file mode 100644 index 0000000..58166d1 --- /dev/null +++ b/assets/branding/framely-mark-light.svg @@ -0,0 +1,7 @@ + + Framely + Sky blue pixel window sprite for dark backgrounds. + + + + diff --git a/assets/branding/framely-mark.svg b/assets/branding/framely-mark.svg new file mode 100644 index 0000000..6efbfe6 --- /dev/null +++ b/assets/branding/framely-mark.svg @@ -0,0 +1,7 @@ + + Framely + Pixel window sprite with two square eyes and a raised hand. + + + + diff --git a/assets/branding/framely.ico b/assets/branding/framely.ico new file mode 100644 index 0000000000000000000000000000000000000000..886bc21d6d9055da133e078b92cec35e106f116b GIT binary patch literal 6276 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 PLa=qy&QI@dj6DB$;T4JV literal 0 HcmV?d00001 diff --git a/assets/branding/icons/128.png b/assets/branding/icons/128.png new file mode 100644 index 0000000000000000000000000000000000000000..446cdd091a3c95600eb9bccda6ae676eb7e0ae4a GIT binary patch literal 1296 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV7cz;;uumf=k2Y7dBN@?$37aH z-Mu%!z{K}KU2bk1PtNCBBanLy|_s-z> zu09cC@wwJ#PX4&}`LD_Wy>DkE@7K$iAG%n+$1eI!&9|@0pYrZcy#Lk3nx(eD?}^^Y zpWl_k=i1b6sP)v1`SF!^--q~Dzs?sNJkGLLT<=rK>Q(=`S_S^STekS-)g9m7rT&<0 z{ZNmmb$NeCD0_SL?c)#cZIx!!(YM=a{!oo4#P#2Un}@4|vtrmAy7N0$%dNl1IgL@~ ztk`bTtN&YPCpi8nGq|;??%{cs(+q0z>+Ve~{>@TmQB}R8y!?$?dUx%r*X0lG8x0q) z+Vx&-`RB|E?+T%J&4%YL7MuSxh;P~6Z2izqLbz^6`6IQ>JNdUCTvp7IAj4$9$0*Ux z(BsT-Op)QSKtmzN0Sf}c*Jm^Q`}g+whurk zu$`2g`OH$#!EutY(b;09h9;Htlv&SR7+5{eoiI7y$-p&f&cw`l!i*v+mdclH)R{Cq zirkh}`mqE|c+%tZc@D<{1+W33%pI};RWoiBU!NMGt$s4br`U|;!i05GC#UEwvy^7a z@VI`;(@6aC99~9~CD~6^W_D+uYh#eQw6$o`nWHA@!MqA?RT-Qwxs`7Q?+Wvqv~$L@ z9F7Hwu|%3w_L|YdGfw8L__E#uc6w)+7R!Z+lq<#`O6TI(6IhXw#Y~=rxl#^~v&x$^AKpHAp@u8|=i{s4|4x}RZg`r`6u8?O}TMK5U=f5$H^u?_oP|>6=M1t(w2SgaJz*_@XCaDw<7NI z{1$y({8(}ZtI%YJHkDt~I0P6t65Xc1h*e^+VyQSj*N|JH_B7YNAEFG?3MA_1sYW(V iwPFz#?EN_PL0&(1dT_(u`jq9OgUMRQ&SlTBA)$`iYqCEq6{TAa<*=x)00o0VJ Ugk%|%ng9R*07*qoM6N<$f(A2hdH?_b literal 0 HcmV?d00001 diff --git a/assets/branding/icons/256.png b/assets/branding/icons/256.png new file mode 100644 index 0000000000000000000000000000000000000000..1e4ce4d64407926c924e95ad6e355a93f136557a GIT binary patch literal 3191 zcmeHKdr(t%7CtvdNEL!WsfZEKI0_Zyp(8>KL5+YdAP=d6KvGMff(k+-mCRS* zJ5NQY7Q1-6k~s;Jc%%2uZ<{qh>^ZmY3s2wm{A{!I8yYH(7Iqxlw0UChgjp&sXX=uR zS)E%zijv1L*l(pgozu)x&Su70y;wNsk9h4>!U7 z9xeM=N`ScWh2s2)%y;1N@Z9rfldA5@3;YaCwAK-l!_V22Z|ig4;HY~#-MWW!yTt-} zQ*6>g6Am}%9EqNt=dJMA{;0-})meP(^Hm{DQL68s=+r`ogl_HthU=`?j61xg*b`+e z-T*bSA?mI?Y~op{&B>BxtKEM5o9Tztw~8hg2#nD4jV4YGZPD~;1b@Sh|A}BhW-THa zF}yQTxhQoXB)-UO@%!XizGt5((xz!R1xcJZA&Z+Xo>{$GRSw>PGV@f$!$<82GRvdO z;c@Kjj`SLCaf&9>bVxv5u_=UV&0Y@5j%h(|aC7Bi)FrZdd1C1%mL;gir!ykg-V~T1 zJf)j#0m(puHRc>v+N8g4gR>kM;iFkF%%`6AO|wXR)O6csjJjwU-b?$oJrQpDq2Vh19^h*NRy1WjLX#Mbb zhI?ejZGuiMFvNNfnL+}BI1_Pj5HHzCfG3SpSxGJc9-WaejZ1C5)uPVj6c0Wy40gvQ_K%-qi|@jv4U&h6HW|_ zpF7vlT`9jvSplT3tyyG8!;0EDsUogBsl(b7h|0??XY%HC^=HVFYvUSXa|mFFbq@Qs zA#r0jCmX_R2S4}0qgm{a60gdkR!yIds1AS9N5)AuQ`T?1pM?HV_@W^3zF8-Q`)pXXzHwf(?tKS%q7Q@ocz1AD}j`|)ttrfzC8GzIg9!R7|J{US3`}KLiN&$ z!Ga!6U0@HBT~qBNO)spVkbx9F-nS(zf3%^_+FrxFqUJ&3M@_K{nUQ6Re)iSJ`Pu#J zaFWZ|MaNuL;l+5>86kaDTn7QT|E)v|^+3CpR$sBUlzVj7V!nM&=C!eSiC@>el8AEA zE06`*Y(}nXXKxRgdDE}!qA9EkGT|i^DPLh`tWstM)r3Uu$;s8a264Qhoknq%vMOI~ z0WGXB^lk`X9iRlO+h}7;5NC5tV%K}^@2!dX*C6)4fWY9Fbb6|+jg^Xc!a9Hby!^;0 zF?*~~xVEI~z*+rlyd;&SAc|8~)n*qzu<~b0-r@INv{$)G`!{6&%*y|#`K33n*|_Q@ z@X+rSp&xZ?BQ20eR<<162IBd@UA}*%>tc)KKYU5Lb%9wFr04kEBxhmOy0>wRRbfQ$ zJeK!1XkXdZ@|Eyj1pzILcDD@cSz>`WpLH-Kw|SJTfSAwZjOeh~o%-<+l6Vd(-XEc0 zdtTO=e@v##`dZbwrngUJMJ7Ycl}(mqy|*q$#0uhhJSfPXWE4c1aFdFfYFu3Pm8Dr9 zuqh-dOD($dmAT+97B*h8Po*dO>Q+txhZkP*=yh%gB>tN zeR`E8POAB~`TPfe9pWy|V~Rql$mzQ`Hd2Q~^%;|QJKD5*>+zE}!9KhuGZ+-u1oSTS zBATo%(lRu*aBwi~<0G|i(|fsKwmY}`w$r`zFOnC*j+@30*OZFsPVgZog03qTrN*dQ zv%nb}K;yAn`54Z*j*rRb&IKm>0rI@Nk65(Wo{WKntYqQBJRB)(CpqjT-!ILU-y z-miRMDZ(Hi45N@%l-#i9Cw6wGc=vYd4vy$4e3_2cW*v^dR%a3;}Z3+4wN@6G#36qiUq|ZJ)pvj?N z$@OkTR1s#mX}a`ixXfX2Efd?9)*n)uo1>iX^(~iOr&jifLS%!D8IOjN6#gdKXF5aC Yst+!ev_{73e<)!4wg6hq7RIr^0b9~rUH||9 literal 0 HcmV?d00001 diff --git a/assets/branding/icons/32.png b/assets/branding/icons/32.png new file mode 100644 index 0000000000000000000000000000000000000000..6729736dd9b2dc4ef3221e822cfdaa173df424c2 GIT binary patch literal 327 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 Jg`d+e{R4vuOA-J8 literal 0 HcmV?d00001 diff --git a/assets/branding/icons/64.png b/assets/branding/icons/64.png new file mode 100644 index 0000000000000000000000000000000000000000..8f233802b633880188f25aa4e17672777f32679a GIT binary patch literal 482 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU|i|x;uumf=k4wNeun}?j(;q+ z7GmSEX~<8I^cAmVFHZ~;k~?rQR_OJbcl(<4u3r4cS;HtL+cW!$^I_AoTg|=;L`l7S zKiffj%lGr@JbIxa8Z|TLr`q!^ETut{f#yJY&APuW#(hg<4g<;Eeux0kNz8phm_5)>{ThYc^SbF zA-+JT*zoic?n^iOQsctQd_%$7FL*KBK4ZD`Gvk81hGjq#w{a}E&DK)#*oxr_W561& z#aS0$-&5yh@MXTCcW+Pr%(r_dF)%dDV!R~>*3Hn+e-0Gp3cWvneE;uf?)Bk;iu}oo z@s38Ezv}olRMq|HyyNJxOZL4+eZ{Q*Ki;a%>K9wGtY+700nYz#($fz<=nMPHaB2G` U*-ra|k3c4Ry85}Sb4q9e01dIstpET3 literal 0 HcmV?d00001 diff --git a/docs/developer-guide/manifest.md b/docs/developer-guide/manifest.md index 7eb02b9..f96bfee 100644 --- a/docs/developer-guide/manifest.md +++ b/docs/developer-guide/manifest.md @@ -57,9 +57,18 @@ | `autostart` | 默认 false,按需启动;true 表示启动时常驻 | | `restart` | `on-failure`(默认)或 `never` | | `restartLimit` | 连续失败阈值 1–10,默认 3 | +| `memoryLimitMiB` | 内存上限,默认 512 MiB;1–4294967295 的整数,不能用 0 或 null 取消限制 | 省略 backend 可制作纯 UI 插件。系统/Python 第三方依赖不会自动安装。运行用户变化需用户确认,数据目录按运行身份区分,不自动迁移。 +`backend.memoryLimitMiB` 映射到 systemd 的 `MemoryMax`,限制后端及其所有子进程的合计内存;它是上限,不会预分配内存。独立生命周期钩子沿用该值,没有后端的钩子使用 512 MiB。安装计划、CLI 和已安装插件管理页展示上限。省略或填写 512 时打包省略该字段,保持旧包兼容;使用其他值需要支持此字段的新版宿主及插件数据库。 + +例如,为较长回放声明 2 GiB: + +```json +{"backend": {"entry": "backend", "memoryLimitMiB": 2048}} +``` + ## `ui` `quickPage` 是可选包内页面 bundle 路径;`windows` 默认空对象,最多 8 个窗口,key 必须是合法 ID。每个窗口: diff --git a/docs/en/developer-guide/manifest.md b/docs/en/developer-guide/manifest.md index 72e2da7..9cab7e6 100644 --- a/docs/en/developer-guide/manifest.md +++ b/docs/en/developer-guide/manifest.md @@ -57,9 +57,18 @@ Legacy `category` is accepted only for compatibility and no longer exported; use | `autostart` | false by default for on-demand startup; true for resident startup | | `restart` | `on-failure` (default) or `never` | | `restartLimit` | Consecutive failure threshold 1–10, default 3 | +| `memoryLimitMiB` | Memory limit, default 512 MiB; integer 1–4294967295, no 0/null for unlimited memory | Omit backend for UI-only plugins. System/third-party Python dependencies are not installed automatically. Runtime-user changes require confirmation and use separate data directories without automatic migration. +`backend.memoryLimitMiB` maps to systemd `MemoryMax` for the combined memory of the backend and all its child processes. It is a ceiling, not a memory reservation. Independent lifecycle hooks inherit this limit; hooks without a backend use 512 MiB. The install plan, CLI and installed-plugin management view display the limit. Omitted or explicit 512 values are omitted when packing to preserve compatibility with older hosts; other values require a host and plugin database supporting this field. + +For example, declare 2 GiB for a longer replay buffer: + +```json +{"backend": {"entry": "backend", "memoryLimitMiB": 2048}} +``` + ## UI Optional `quickPage` points to a payload bundle. `windows` defaults to an empty map with at most eight windows; keys must be valid IDs: diff --git a/docs/en/releases.md b/docs/en/releases.md index 1784533..37df0b3 100644 --- a/docs/en/releases.md +++ b/docs/en/releases.md @@ -36,8 +36,8 @@ Deleted `/home` data cannot be recovered. OS updates may require manual interven Device and installer share the repository with independent versions/tags/workflows: -- Device: root Cargo version, `v` (currently `v0.4.2-preview.5`), `.github/workflows/release.yml`, Linux ARM64 and pinned CEF. -- Installer: `installer/Cargo.toml`, `installer-v` (currently `installer-v0.4.1-preview.3`), `.github/workflows/installer-release.yml`, Linux x64/ARM64, Windows x64, macOS Intel/Apple Silicon, pinned GPUI Kit. +- Device: root Cargo version, `v` (currently `v0.4.2-preview.6`), `.github/workflows/release.yml`, Linux ARM64 and pinned CEF. +- Installer: `installer/Cargo.toml`, `installer-v` (currently `installer-v0.4.1-preview.4`), `.github/workflows/installer-release.yml`, Linux x64/ARM64, Windows x64, macOS Intel/Apple Silicon, pinned GPUI Kit. Versions need not match or ship together. Update the relevant Cargo.lock with version changes. Installer package/macOS versions use the installer version. @@ -45,11 +45,11 @@ Both workflows cache dependencies/builds by product/platform/toolchain/configura ```bash # After committing and pushing the intended source: -git tag v0.4.2-preview.5 -git push origin v0.4.2-preview.5 +git tag v0.4.2-preview.6 +git push origin v0.4.2-preview.6 # Independent installer release: -git tag installer-v0.4.1-preview.3 -git push origin installer-v0.4.1-preview.3 +git tag installer-v0.4.1-preview.4 +git push origin installer-v0.4.1-preview.4 ``` Prerelease suffixes create GitHub Prereleases without Latest. Installer releases are never Latest. Only stable device releases become Latest, keeping default installer/update URLs on the device product. The desktop installer filters device archive names and does not mistake ARM64 installer packages for device runtimes. Download desktop installers from their explicit installer tags. diff --git a/docs/en/user-guide/installation.md b/docs/en/user-guide/installation.md index 1a13cc2..4860114 100644 --- a/docs/en/user-guide/installation.md +++ b/docs/en/user-guide/installation.md @@ -18,6 +18,8 @@ Connect your computer and Frame to the same network. Enable developer mode and S 4. Enable testing releases to select a Preview. Installer and device versions are independent. 5. Confirm the device, version and operation on the maintenance page. Alternatively, select a local device archive and its external `SHA256SUMS`. +On Linux, run `bash install-desktop-entry.sh` from the extracted installer directory to add an entry with the Framely icon to your user application menu. The entry points to that directory; run the script again after moving it. + Downloads are checked locally and again on the device. Passwords are not saved in configuration. Observe startup and rendering on Frame after installation. ## Option 2: install from Frame's SSH terminal diff --git a/docs/releases.md b/docs/releases.md index 1a0cc61..a9b47c3 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -40,8 +40,8 @@ framely call system.source.save '{"source":{"url":"https://example.org/framely-r Framely 和桌面安装器保留在同一仓库,使用独立的版本号、标签和发布工作流: -- Framely:版本由根 `Cargo.toml` 定义,标签为 `v<版本>`(当前 `v0.4.2-preview.5`);`.github/workflows/release.yml` 只构建 Frame Linux ARM64 发行,使用锁定的 CEF 并执行自动检查。 -- 安装器:版本由 `installer/Cargo.toml` 定义,标签为 `installer-v<版本>`(当前 `installer-v0.4.1-preview.3`);`.github/workflows/installer-release.yml` 只构建 Linux x64/ARM64、Windows x64、macOS x64/ARM64 的原生安装器,使用锁定的 GPUI Kit。 +- Framely:版本由根 `Cargo.toml` 定义,标签为 `v<版本>`(当前 `v0.4.2-preview.6`);`.github/workflows/release.yml` 只构建 Frame Linux ARM64 发行,使用锁定的 CEF 并执行自动检查。 +- 安装器:版本由 `installer/Cargo.toml` 定义,标签为 `installer-v<版本>`(当前 `installer-v0.4.1-preview.4`);`.github/workflows/installer-release.yml` 只构建 Linux x64/ARM64、Windows x64、macOS x64/ARM64 的原生安装器,使用锁定的 GPUI Kit。 两个版本不需要相同,也不需要同时发布。修改各自版本时同步对应的 `Cargo.lock`;安装器包名及 macOS 应用版本使用安装器版本。 @@ -51,12 +51,12 @@ Framely 和桌面安装器保留在同一仓库,使用独立的版本号、标 ```bash # 提交并推送代码后,选择需要发布的产品 -git tag v0.4.2-preview.5 -git push origin v0.4.2-preview.5 +git tag v0.4.2-preview.6 +git push origin v0.4.2-preview.6 # 独立发布安装器,不触发 Framely 构建 -git tag installer-v0.4.1-preview.3 -git push origin installer-v0.4.1-preview.3 +git tag installer-v0.4.1-preview.4 +git push origin installer-v0.4.1-preview.4 ``` 带 `-preview.N` 等预发布后缀的标签会发布为 GitHub Prerelease,不设置为 Latest。安装器 Release 始终设置为非 Latest;只有 Framely 正式 Release 设置为 Latest。因此设备安装入口和默认更新清单的 `releases/latest/download/...` 继续指向 Framely 本体。安装器读取发行列表时按本体包名筛选,不把 Linux ARM64 安装器误当成设备发行包。安装器下载应使用具体的 `installer-v<版本>` Release。 diff --git a/docs/user-guide/installation.md b/docs/user-guide/installation.md index c33c401..a350a57 100644 --- a/docs/user-guide/installation.md +++ b/docs/user-guide/installation.md @@ -18,6 +18,8 @@ 4. 如果要安装 Preview,开启显示测试版;安装器版本与设备版版本相互独立。 5. 在“安装与维护”确认设备、目标版本及操作,等待完成。也可选择本地运行包和对应的外部 `SHA256SUMS`。 +Linux 用户可在安装器解压目录执行 `bash install-desktop-entry.sh`,把带 Framely 图标的入口添加到当前用户的应用菜单。入口指向解压目录;移动目录后需重新执行脚本。 + 安装器会校验下载内容,并在设备端再次校验。密码不保存到配置文件。连接后仍需在实际设备上观察 Framely 的启动和显示。 ## 方式二:在 Frame 的 SSH 终端安装 diff --git a/installer/Cargo.lock b/installer/Cargo.lock index dfc7ba9..1350647 100644 --- a/installer/Cargo.lock +++ b/installer/Cargo.lock @@ -1985,7 +1985,7 @@ dependencies = [ [[package]] name = "framely-installer" -version = "0.4.1-preview.3" +version = "0.4.1-preview.4" dependencies = [ "anyhow", "base64 0.22.1", @@ -2003,6 +2003,7 @@ dependencies = [ "tempfile", "ureq", "url", + "winresource", "zeroize", ] @@ -8755,6 +8756,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winresource" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +dependencies = [ + "toml 1.1.6+spec-1.1.0", + "version_check", +] + [[package]] name = "wio" version = "0.2.2" diff --git a/installer/Cargo.toml b/installer/Cargo.toml index 6cebbb8..b4617e1 100644 --- a/installer/Cargo.toml +++ b/installer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "framely-installer" -version = "0.4.1-preview.3" +version = "0.4.1-preview.4" edition = "2024" license = "AGPL-3.0-only" @@ -33,6 +33,9 @@ tempfile = "3" zeroize = "1" directories = "6" +[build-dependencies] +winresource = "=0.1.31" + [profile.release] lto = "thin" strip = true diff --git a/installer/README.en.md b/installer/README.en.md index 55bca02..f1bdb65 100644 --- a/installer/README.en.md +++ b/installer/README.en.md @@ -56,10 +56,12 @@ FRAMELY_INSTALLER_PREVIEW_DIR=installer/dist/previews installer/target/debug/fra Preview requires an available GPU or software Vulkan renderer. Default release builds do not include preview support. -Installer versions are managed independently in this directory's `Cargo.toml` and `Cargo.lock`. Pushing an `installer-v` tag (currently `installer-v0.4.1-preview.3`) triggers `.github/workflows/installer-release.yml` to build and publish only the installer. The core uses `v` tags and a separate workflow. Manual Actions runs produce build artifacts only. Installer releases do not become the repository's Latest release, preserving Frame's default install/update URLs. +Installer versions are managed independently in this directory's `Cargo.toml` and `Cargo.lock`. Pushing an `installer-v` tag (currently `installer-v0.4.1-preview.4`) triggers `.github/workflows/installer-release.yml` to build and publish only the installer. The core uses `v` tags and a separate workflow. Manual Actions runs produce build artifacts only. Installer releases do not become the repository's Latest release, preserving Frame's default install/update URLs. Actions builds Linux x64/ARM64, Windows x64, and macOS Intel/Apple Silicon. macOS produces an `.app` ZIP, Windows an EXE ZIP, and Linux a tar.gz. Apple notarization and Windows code signing are not configured; verify first-launch behavior on each system before publishing. +The titlebar and application icons share the pixel logo. Windows builds embed the ICO in the EXE; macOS packaging uses the system `iconutil` to generate and configure `.icns`. On Linux, run `bash install-desktop-entry.sh` from the extracted archive to register an icon-bearing entry in the current user's application menu. The entry points to that directory; rerun the script after moving it. + Automated tests and Actions builds validate software and artifacts. Discovery, login, permissions, update, rollback, and uninstall still need acceptance testing on actual computers and Frame hardware. Discovery queries `frame.local` mDNS address records directly. It does not require an advertised SSH service or system DNS support for `.local`. Run the same discovery logic independently for diagnosis: diff --git a/installer/README.md b/installer/README.md index c59c11a..a2b792f 100644 --- a/installer/README.md +++ b/installer/README.md @@ -56,10 +56,12 @@ FRAMELY_INSTALLER_PREVIEW_DIR=installer/dist/previews installer/target/debug/fra 预览依赖可用的 GPU 或软件 Vulkan 渲染器;默认发行构建不包含此预览功能。 -安装器版本由本目录的 `Cargo.toml` 和 `Cargo.lock` 独立管理,不跟随 Framely 本体版本。推送 `installer-v<版本>` 标签(当前 `installer-v0.4.1-preview.3`)触发 `.github/workflows/installer-release.yml`,只构建并发布安装器;本体使用 `v<版本>` 标签和独立工作流。也可在 Actions 手动运行安装器工作流,仅生成构建产物。安装器 Release 不占用仓库的 Latest,以免影响 Frame 的默认安装和更新地址。 +安装器版本由本目录的 `Cargo.toml` 和 `Cargo.lock` 独立管理,不跟随 Framely 本体版本。推送 `installer-v<版本>` 标签(当前 `installer-v0.4.1-preview.4`)触发 `.github/workflows/installer-release.yml`,只构建并发布安装器;本体使用 `v<版本>` 标签和独立工作流。也可在 Actions 手动运行安装器工作流,仅生成构建产物。安装器 Release 不占用仓库的 Latest,以免影响 Frame 的默认安装和更新地址。 Actions 构建 Linux x64/ARM64、Windows x64、macOS Intel/Apple Silicon。macOS 输出 `.app` ZIP,Windows 输出 EXE ZIP,Linux 输出 tar.gz。当前未配置 Apple 公证或 Windows 代码签名,发布前应分别验证系统的首次启动体验。 +安装器标题栏和应用图标使用统一的像素 Logo。Windows 构建将 ICO 嵌入 EXE,macOS 打包使用系统 `iconutil` 生成并配置 `.icns`。Linux 解压后可在解压目录执行 `bash install-desktop-entry.sh`,把带图标的入口注册到当前用户的应用菜单;入口指向该目录,移动后需重新执行脚本。 + 所有自动测试和 Actions 构建只能验证软件及产物;仍需在真实电脑和 Frame 上完成扫描、登录、权限、升级回滚及卸载验收。 扫描会直接查询 `frame.local` 的 mDNS 地址记录,不要求设备广播 SSH 服务,也不依赖电脑的系统 DNS 支持 `.local`。可单独运行与界面相同的发现逻辑进行诊断: diff --git a/installer/build.rs b/installer/build.rs new file mode 100644 index 0000000..33d4a4f --- /dev/null +++ b/installer/build.rs @@ -0,0 +1,11 @@ +fn main() { + println!("cargo:rerun-if-changed=../assets/branding/framely.ico"); + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + winresource::WindowsResource::new() + .set_icon("../assets/branding/framely.ico") + .set("ProductName", "Framely Installer") + .set("FileDescription", "Framely Installer") + .compile() + .expect("Could not embed Framely Windows application icon"); + } +} diff --git a/installer/install-desktop-entry.sh b/installer/install-desktop-entry.sh new file mode 100644 index 0000000..ff738b8 --- /dev/null +++ b/installer/install-desktop-entry.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Register the extracted Linux installer with its matching application icon. +set -euo pipefail +base=$(cd -- "$(dirname -- "$0")" && pwd) +python3 - "$base" <<'PY' +import os, pathlib, shutil +base = pathlib.Path(__import__('sys').argv[1]) +binary = base / 'framely-installer' +if not binary.is_file(): + raise SystemExit('Run this script from an extracted Framely Linux installer archive.') +data = pathlib.Path(os.environ.get('XDG_DATA_HOME') or pathlib.Path.home() / '.local/share') +apps = data / 'applications' +icons = data / 'icons/hicolor/scalable/apps' +apps.mkdir(parents=True, exist_ok=True) +icons.mkdir(parents=True, exist_ok=True) +shutil.copy2(base / 'assets/branding/framely-app-icon.svg', icons / 'org.framely.installer.svg') +# Desktop entries parse backslash escapes before parsing the quoted Exec value. +quoted = str(binary).replace('%', '%%').replace('\\', '\\\\') +for character in ('"', '`', '$'): + quoted = quoted.replace(character, '\\' + character) +quoted = quoted.replace('\\', '\\\\') +entry = apps / 'org.framely.installer.desktop' +entry.write_text(f'''[Desktop Entry] +Type=Application +Name=Framely Installer +Name[zh_CN]=Framely 安装器 +Comment=Install and manage Framely on Steam Frame +Comment[zh_CN]=安装和维护 Steam Frame 上的 Framely +Exec=/usr/bin/env "{quoted}" +Icon=org.framely.installer +Terminal=false +Categories=Utility; +StartupWMClass=org.framely.installer +''') +print(f'Registered Framely Installer: {entry}') +PY diff --git a/installer/src/main.rs b/installer/src/main.rs index b574843..1002065 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -839,6 +839,7 @@ fn main() { gpui_kit::application() .with_assets(gpui_kit::assets::AllAssets) .run(|cx| { + cx.set_app_identity("org.framely.installer", "Framely Installer"); initialize_theme(cx); cx.on_window_closed(|cx, _| { if cx.windows().is_empty() { @@ -847,6 +848,7 @@ fn main() { }) .detach(); let options = WindowOptions { + app_id: Some("org.framely.installer".into()), window_min_size: Some(size(px(980.), px(720.))), titlebar: Some(TitlebarOptions { title: Some("Framely Installer".into()), diff --git a/installer/src/ui.rs b/installer/src/ui.rs index 8db479a..4f77504 100644 --- a/installer/src/ui.rs +++ b/installer/src/ui.rs @@ -1160,24 +1160,12 @@ impl Render for Installer { .h_full() .px_5() .child( - div() - .w(px(26.)) - .h(px(26.)) - .border_2() - .border_color(rgb(BLUE)) - .rounded(px(5.)) - .flex() - .items_center() - .justify_center() - .child( - div().w(px(10.)).h(px(10.)).bg(rgb(BLUE)).rounded(px(2.)), - ), - ) - .child( - div() - .font_weight(FontWeight::SEMIBOLD) - .text_lg() - .child("Framely"), + svg() + .data(include_bytes!("../../assets/branding/framely-logo.svg")) + .w(px(150.)) + .h(px(36.)) + .flex_none() + .text_color(rgb(BLUE)), ) .child(div().text_sm().text_color(rgb(MUTED)).child("安装器")) .child(div().flex_1()) diff --git a/native/brand_mask.h b/native/brand_mask.h new file mode 100644 index 0000000..cf580dc --- /dev/null +++ b/native/brand_mask.h @@ -0,0 +1,54 @@ +// Generated by tools/build-branding.mjs from assets/branding/framely-mark.svg. +#pragma once +namespace framely_brand { +inline constexpr unsigned char mask[48 * 48] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,255,255,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,255,255,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,255,255,0,0,0,0,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,255,255,0,0,0,0,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +}; +} diff --git a/native/host.cpp b/native/host.cpp index d53cbd7..17129ae 100644 --- a/native/host.cpp +++ b/native/host.cpp @@ -13,6 +13,7 @@ #include "notification_geometry.h" #include "keyboard_state.h" #include "keyboard_input.h" +#include "brand_mask.h" #include #include #include @@ -73,17 +74,12 @@ static void execute(cef_frame_t* f,const std::string& code){if(!f||!f->is_valid( static std::vector icon(bool hover,bool active){ std::vector p(128*128*4); auto rounded=[](float x,float y,float half,float radius){float a=std::abs(x)-half+radius,b=std::abs(y)-half+radius;return std::hypot(std::max(a,0.f),std::max(b,0.f))+std::min(std::max(a,b),0.f)-radius;}; - auto circle=[](float x,float y,float cx,float cy){return std::hypot(x-cx,y-cy)-10.f;}; for(int y=0;y<128;y++)for(int x=0;x<128;x++){ float coverage=std::clamp(.5f-rounded(x-63.5f,y-63.5f,64.f,9.6f),0.f,1.f); - float gx=(x-63.5f)*1.15f+71.f,gy=(y-63.5f)*1.15f+57.f; - float qx=std::abs(gx-64.f)-23.f,qy=std::abs(gy-64.f)-23.f; - float d=std::hypot(std::max(qx,0.f),std::max(qy,0.f))+std::min(std::max(qx,qy),0.f)-3.f; - d=std::min(d,std::min(circle(gx,gy,64,34),circle(gx,gy,94,64))); - d=std::max(d,std::max(-circle(gx,gy,38,64),-circle(gx,gy,64,90))); - float mark=std::clamp(2.7f-std::abs(d),0.f,1.f); + int gx=std::min(47,int((x+.5f)*48/128)),gy=std::min(47,int((y+.5f)*48/128)); + float mark=framely_brand::mask[gy*48+gx]/255.f; int i=(y*128+x)*4; - const int normal[3]={14,20,27},highlight[3]={61,68,80},muted[3]={139,146,154},bright[3]={220,222,223}; + const int normal[3]={23,25,28},highlight[3]={61,68,80},muted[3]={147,197,237},bright[3]={240,241,243}; for(int c=0;c<3;c++){float bg=(hover||active)?highlight[c]:normal[c];float fg=(hover||active)?bright[c]:muted[c];p[i+c]=uint8_t(bg+(fg-bg)*mark);} // 32px-wide, 6px-high tab indicator is clipped to its upper 3px by the BarSurface. if(active){float qx=std::abs(x-63.5f)-20.8f,qy=std::abs(y-129.6f);float line=std::clamp(5.3f-std::hypot(std::max(qx,0.f),qy),0.f,1.f);const int blue[3]={26,159,255};for(int c=0;c<3;c++)p[i+c]=uint8_t(p[i+c]+(blue[c]-p[i+c])*line);} diff --git a/package-lock.json b/package-lock.json index 249f95a..eacbd8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,8 +4,8 @@ "requires": true, "packages": { "": { - "license": "AGPL-3.0-only", "name": "framely-workspace", + "license": "AGPL-3.0-only", "workspaces": [ "sdk", "ui", @@ -16,6 +16,7 @@ "react-dom": "19.2.0" }, "devDependencies": { + "@resvg/resvg-js": "2.6.2", "@types/react": "19.2.0", "@types/react-dom": "19.2.0", "esbuild": "0.25.10", @@ -23,9 +24,9 @@ } }, "examples/showcase": { - "license": "AGPL-3.0-only", "name": "@framely/showcase", "version": "0.4.0", + "license": "AGPL-3.0-only", "dependencies": { "@framely/sdk": "0.4.0" } @@ -484,6 +485,234 @@ "resolved": "ui", "link": true }, + "node_modules/@resvg/resvg-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", + "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@resvg/resvg-js-android-arm-eabi": "2.6.2", + "@resvg/resvg-js-android-arm64": "2.6.2", + "@resvg/resvg-js-darwin-arm64": "2.6.2", + "@resvg/resvg-js-darwin-x64": "2.6.2", + "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", + "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", + "@resvg/resvg-js-linux-arm64-musl": "2.6.2", + "@resvg/resvg-js-linux-x64-gnu": "2.6.2", + "@resvg/resvg-js-linux-x64-musl": "2.6.2", + "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", + "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", + "@resvg/resvg-js-win32-x64-msvc": "2.6.2" + } + }, + "node_modules/@resvg/resvg-js-android-arm-eabi": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", + "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-android-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", + "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", + "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", + "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", + "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", + "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", + "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", + "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-arm64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", + "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-ia32-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", + "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-x64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", + "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@types/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", @@ -595,18 +824,18 @@ } }, "sdk": { - "license": "AGPL-3.0-only", "name": "@framely/sdk", "version": "0.4.0", + "license": "AGPL-3.0-only", "peerDependencies": { "react": "19.2.0", "react-dom": "19.2.0" } }, "ui": { - "license": "AGPL-3.0-only", "name": "@framely/ui", - "version": "0.4.0" + "version": "0.4.0", + "license": "AGPL-3.0-only" } } } diff --git a/package.json b/package.json index 7a2ec67..3df39c2 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,28 @@ { - "license":"AGPL-3.0-only", - "name":"framely-workspace","private":true,"type":"module", - "workspaces":["sdk","ui","examples/showcase"], - "scripts":{"build":"node tools/build-ui.mjs","typecheck":"tsc --noEmit","plugin:init":"node tools/plugin-dev.mjs init"}, - "dependencies":{"react":"19.2.0","react-dom":"19.2.0"}, - "devDependencies":{"@types/react":"19.2.0","@types/react-dom":"19.2.0","typescript":"5.9.3","esbuild":"0.25.10"} + "license": "AGPL-3.0-only", + "name": "framely-workspace", + "private": true, + "type": "module", + "workspaces": [ + "sdk", + "ui", + "examples/showcase" + ], + "scripts": { + "build": "node tools/build-ui.mjs", + "branding:build": "node tools/build-branding.mjs", + "typecheck": "tsc --noEmit", + "plugin:init": "node tools/plugin-dev.mjs init" + }, + "dependencies": { + "react": "19.2.0", + "react-dom": "19.2.0" + }, + "devDependencies": { + "@resvg/resvg-js": "2.6.2", + "@types/react": "19.2.0", + "@types/react-dom": "19.2.0", + "esbuild": "0.25.10", + "typescript": "5.9.3" + } } diff --git a/src/main.rs b/src/main.rs index 7dd7209..3a3052b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -204,7 +204,7 @@ fn run() -> Result<()> { return Err(e); } }; - let items:Vec<_>=prepared.plan.items.iter().map(|i|json!({"id":i.manifest.id,"name":i.manifest.name,"version":i.manifest.version,"action":i.action,"runAs":i.manifest.run_as(),"source":i.source,"runAsChanged":i.run_as_changed})).collect(); + let items:Vec<_>=prepared.plan.items.iter().map(|i|json!({"id":i.manifest.id,"name":i.manifest.name,"version":i.manifest.version,"action":i.action,"runAs":i.manifest.run_as(),"memoryLimitMiB":i.manifest.memory_limit_mib(),"source":i.source,"runAsChanged":i.run_as_changed})).collect(); println!( "{}", serde_json::to_string_pretty( diff --git a/src/model.rs b/src/model.rs index 5e7279e..dddb91b 100644 --- a/src/model.rs +++ b/src/model.rs @@ -27,6 +27,19 @@ pub struct Backend { pub restart: RestartPolicy, #[serde(default = "restart_limit")] pub restart_limit: u32, + #[serde( + rename = "memoryLimitMiB", + default = "default_memory_limit_mib", + skip_serializing_if = "is_default_memory_limit_mib" + )] + pub memory_limit_mib: u32, +} +pub const DEFAULT_MEMORY_LIMIT_MIB: u32 = 512; +fn default_memory_limit_mib() -> u32 { + DEFAULT_MEMORY_LIMIT_MIB +} +fn is_default_memory_limit_mib(value: &u32) -> bool { + *value == DEFAULT_MEMORY_LIMIT_MIB } fn restart_limit() -> u32 { 3 @@ -252,6 +265,10 @@ impl Manifest { } if let Some(b) = &self.backend { check(&b.entry)?; + ensure!( + b.memory_limit_mib > 0, + "Backend memoryLimitMiB must be a positive integer" + ); ensure!((1..=10).contains(&b.restart_limit), "Invalid restart limit"); ensure!( b.args.len() <= 64 && b.args.iter().all(|a| a.len() <= 4096 && !a.contains('\0')), @@ -317,6 +334,12 @@ impl Manifest { } Ok(()) } + pub fn memory_limit_mib(&self) -> u32 { + self.backend + .as_ref() + .map(|b| b.memory_limit_mib) + .unwrap_or(DEFAULT_MEMORY_LIMIT_MIB) + } pub fn run_as(&self) -> RunAs { self.backend .as_ref() diff --git a/src/process.rs b/src/process.rs index 3662eec..3589389 100644 --- a/src/process.rs +++ b/src/process.rs @@ -82,7 +82,7 @@ fn command(m: &Manifest, payload: &Path, manager: u32, logs: &Path, unit: &str) "KillMode=control-group".into(), "TimeoutStopSec=5".into(), "TasksMax=128".into(), - "MemoryMax=512M".into(), + format!("MemoryMax={}M", b.memory_limit_mib), "NoNewPrivileges=yes".into(), ] { cmd.arg(format!("--property={p}")); @@ -145,6 +145,7 @@ pub fn hook( autostart: false, restart: RestartPolicy::Never, restart_limit: 3, + memory_limit_mib: m.memory_limit_mib(), }); let unit = format!("framely-hook-{}-{:032x}", m.id, rand::random::()); let cmd = command(&launch, payload, manager, logs, &unit)?; diff --git a/src/session.rs b/src/session.rs index 419ad49..9360093 100644 --- a/src/session.rs +++ b/src/session.rs @@ -177,6 +177,41 @@ impl Agent { ); } } + // Public, compile-time branding only. No other assets bypass network login. + let branding: Option<(&[u8], &str)> = match path.as_str() { + "/assets/branding/framely-mark-light.svg" => Some(( + include_bytes!("../assets/branding/framely-mark-light.svg"), + "image/svg+xml", + )), + "/assets/branding/framely-logo-light.svg" => Some(( + include_bytes!("../assets/branding/framely-logo-light.svg"), + "image/svg+xml", + )), + "/assets/branding/framely-app-icon.svg" => Some(( + include_bytes!("../assets/branding/framely-app-icon.svg"), + "image/svg+xml", + )), + "/assets/branding/framely-app-icon.png" => Some(( + include_bytes!("../assets/branding/framely-app-icon.png"), + "image/png", + )), + "/assets/branding/framely.ico" | "/favicon.ico" => Some(( + include_bytes!("../assets/branding/framely.ico"), + "image/x-icon", + )), + _ => None, + }; + if r.method() == &Method::Get { + if let Some((data, mime)) = branding { + r.respond( + Response::from_data(data) + .with_header(header("Content-Type", mime)) + .with_header(header("X-Content-Type-Options", "nosniff")) + .with_header(header("Cache-Control", "no-cache")), + )?; + return Ok(()); + } + } let configured = remote && self.core("network.password.configured", json!({}))? == true; if remote && (!configured || !self.network_authorized(&r)) { let status = self.core("status", json!({}))?; @@ -291,7 +326,7 @@ impl Agent { return Ok(()); } if r.method() == &Method::Get && (path == "/" || path == "/manager") { - r.respond(Response::from_string(if configured { NETWORK_LOGIN } else { NETWORK_SETUP }).with_header(header("Content-Type","text/html; charset=utf-8")).with_header(header("Content-Security-Policy","default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'")))?; + r.respond(Response::from_string(if configured { NETWORK_LOGIN } else { NETWORK_SETUP }).with_header(header("Content-Type","text/html; charset=utf-8")).with_header(header("Content-Security-Policy","default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'")))?; } else { r.respond( Response::empty(StatusCode(401)) @@ -1138,8 +1173,8 @@ fn network_ipv4_addresses() -> Vec { addresses.into_iter().collect() } -const NETWORK_SETUP: &str = r#"Framely · 设置访问密码

设置访问密码

首次访问网络管理面板,请设置至少 8 个字符的密码。保存后使用该密码登录。

"#; -const NETWORK_LOGIN: &str = r#"Framely

Framely

输入你在设备设置中配置的访问密码。

"#; +const NETWORK_SETUP: &str = r#"Framely · 设置访问密码
Framely

设置访问密码

首次访问网络管理面板,请设置至少 8 个字符的密码。保存后使用该密码登录。

"#; +const NETWORK_LOGIN: &str = r#"Framely

Framely

输入你在设备设置中配置的访问密码。

"#; fn network_listener(agent: Arc, keep_running: impl Fn() -> bool) { let mut server: Option = None; @@ -1607,6 +1642,28 @@ mod tests { let page = ureq::get(&origin).call().unwrap().into_string().unwrap(); assert!(page.contains("设置访问密码")); assert!(page.contains("action=\"/setup\"")); + assert!(page.contains("/assets/branding/framely-logo-light.svg")); + let logo = ureq::get(&format!("{origin}/assets/branding/framely-logo-light.svg")) + .call() + .unwrap(); + assert_eq!(logo.header("Content-Type"), Some("image/svg+xml")); + assert_eq!(logo.header("X-Content-Type-Options"), Some("nosniff")); + assert_eq!( + logo.into_string().unwrap(), + include_str!("../assets/branding/framely-logo-light.svg") + ); + let icon = ureq::get(&format!("{origin}/favicon.ico")).call().unwrap(); + assert_eq!(icon.header("Content-Type"), Some("image/x-icon")); + for path in [ + "/assets/app.js", + "/assets/branding/../app.js", + "/assets/branding/unknown.svg", + ] { + assert!(matches!( + ureq::get(&format!("{origin}{path}")).call(), + Err(ureq::Error::Status(401, _)) + )); + } assert!(!page.contains(password.as_str())); assert!(ureq::post(&format!("{origin}/api")) .set("Origin", &origin) diff --git a/src/tests.rs b/src/tests.rs index 578b59f..9ddc633 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -164,6 +164,48 @@ fn malicious_archive_paths_links_and_duplicates_are_rejected() { .unwrap(); assert!(package::verify(&zip.finish().unwrap().into_inner()).is_err()); } +#[test] +fn backend_memory_limit_defaults_validation_and_round_trip() { + let base = json!({"schemaVersion":1,"apiVersion":1,"id":"test.memory","name":"Memory","author":"Dev","version":"1","backend":{"entry":"backend"},"files":{"backend":package::digest(b"x")}}); + let manifest: Manifest = serde_json::from_value(base.clone()).unwrap(); + manifest.validate().unwrap(); + assert_eq!(manifest.memory_limit_mib(), 512); + // Preserve the old serialized shape when the default is used. + assert!(serde_json::to_value(&manifest).unwrap()["backend"] + .get("memoryLimitMiB") + .is_none()); + for limit in [1, 512, 2048, u32::MAX] { + let mut value = base.clone(); + value["backend"]["memoryLimitMiB"] = json!(limit); + let manifest: Manifest = serde_json::from_value(value).unwrap(); + manifest.validate().unwrap(); + let packed = serde_json::to_value(&manifest).unwrap(); + let restored: Manifest = serde_json::from_value(packed).unwrap(); + assert_eq!(restored.memory_limit_mib(), limit); + } + for limit in [ + json!(0), + json!(-1), + json!(1.5), + json!("2048"), + json!(true), + json!(null), + json!(4294967296u64), + ] { + let mut value = base.clone(); + value["backend"]["memoryLimitMiB"] = limit; + assert!(serde_json::from_value::(value).map_or(true, |m| m.validate().is_err())); + } + let mut ui_only = base; + ui_only.as_object_mut().unwrap().remove("backend"); + assert_eq!( + serde_json::from_value::(ui_only) + .unwrap() + .memory_limit_mib(), + 512 + ); +} + #[test] fn default_identity_and_invalid_identities() { let m = json!({"schemaVersion":1,"apiVersion":1,"id":"test","name":"Test","author":"Dev","version":"1","backend":{"entry":"backend"},"ui":{},"files":{"backend":package::digest(b"x")}}); @@ -469,7 +511,7 @@ for line in sys.stdin: fs::write(payload.join("backend.py"), backend).unwrap(); fs::write(payload.join("page.js"), "test").unwrap(); let hook = json!({"entry":"backend.py"}); - let manifest = json!({"schemaVersion":1,"apiVersion":1,"id":"test.lifecycle","name":"Lifecycle","author":"Test","version":version,"backend":{"entry":"backend.py","runAs":"steamos","autostart":version!="badrollback","restart":restart,"restartLimit":3},"lifecycle":{"onInstall":hook,"onUpdate":hook,"onUninstall":hook,"onCrashCleanup":hook,"onStart":true,"onStop":true,"timeoutSeconds":1},"ui":{"quickPage":"page.js"},"files":{}}); + let manifest = json!({"schemaVersion":1,"apiVersion":1,"id":"test.lifecycle","name":"Lifecycle","author":"Test","version":version,"backend":{"entry":"backend.py","runAs":"steamos","autostart":version!="badrollback","restart":restart,"restartLimit":3,"memoryLimitMiB":if version=="1" {2048} else {512}},"lifecycle":{"onInstall":hook,"onUpdate":hook,"onUninstall":hook,"onCrashCleanup":hook,"onStart":true,"onStop":true,"timeoutSeconds":1},"ui":{"quickPage":"page.js"},"files":{}}); let path = temp.path().join(format!("manifest-{version}.json")); fs::write(&path, serde_json::to_vec(&manifest).unwrap()).unwrap(); let out = temp.path().join(format!("{version}.framely")); @@ -639,6 +681,25 @@ for line in sys.stdin: assert!(arguments.contains(&format!("User={}", unsafe { libc::geteuid() }))); assert!(!arguments.contains("PrivateNetwork=yes")); assert!(arguments.contains("NoNewPrivileges=yes")); + let launches: Vec> = arguments + .lines() + .map(|line| serde_json::from_str(line).unwrap()) + .collect(); + for launch in &launches { + let expected = if launch + .iter() + .any(|arg| arg == "--setenv=FRAMELY_PLUGIN_VERSION=1") + { + "--property=MemoryMax=2048M" + } else { + "--property=MemoryMax=512M" + }; + assert!(launch.iter().any(|arg| arg == expected)); + } + assert!(launches.iter().any(|args| args + .iter() + .any(|arg| arg == "--setenv=FRAMELY_LIFECYCLE=onInstall") + && args.iter().any(|arg| arg == "--property=MemoryMax=2048M"))); assert!(arguments.contains("RuntimeMaxSec=1")); core.shutdown(); } diff --git a/tests/install_review_fixture.tsx b/tests/install_review_fixture.tsx index 8ba9ecf..8c2b66d 100644 --- a/tests/install_review_fixture.tsx +++ b/tests/install_review_fixture.tsx @@ -7,7 +7,7 @@ import '../ui/src/style.css'; const root=createRoot(document.getElementById('root')!); const wait=(ms=600)=>new Promise(resolve=>setTimeout(resolve,ms)); const plugin={id:'tooru.termix',name:'Termix',author:'tooru',version:'0.1.7',description:'通过内网浏览器管理 Frame,提供终端、文件管理与远程桌面功能。',details:'所有设置位于快捷面板。打开大窗口可显示 Termix;内网浏览器仍需账号密码。Web 账号与系统账号分离。',homepage:'https://github.com/Termix-SSH/Termix',backend:{runAs:'steamos'}}; -const camera={id:'framely.framexr-camera',name:'FrameXR Camera',author:'tooru',version:'0.1.9',description:'在 VR 中显示设备相机画面,并控制相机采集与显示设置。',backend:{runAs:'root'}}; +const camera={id:'framely.framexr-camera',name:'FrameXR Camera',author:'tooru',version:'0.1.9',description:'在 VR 中显示设备相机画面,并控制相机采集与显示设置。',backend:{runAs:'root',memoryLimitMiB:2048}}; function info(manifest:any,dependencies:any[]=[]){return {manifest,installedVersion:manifest===plugin?'0.1.6':undefined,plan:{items:[{manifest,action:manifest===plugin?'update':'install'},...dependencies.map(manifest=>({manifest,action:'install'}))],addSources:[],disable:[],enable:[manifest.id],affected:[manifest.id]}};} let installed=0,cancelled=0; async function show(value:any,overrides:any={}) { @@ -26,6 +26,7 @@ async function show(value:any,overrides:any={}) { let modal=await show(info(plugin)); if(!modal.textContent?.includes(plugin.description)||!modal.querySelector('.plugin-links'))throw Error('plugin description or links missing'); if(modal.querySelector('.is-root'))throw Error('steamos shows root warning'); + if(!modal.textContent?.includes('512 MiB'))throw Error('default memory limit missing'); if(modal.textContent?.includes('启用依赖链:')||modal.textContent?.includes('其他受影响的插件:'))throw Error('redundant single-plugin effects'); console.log('FRAMELY_PREVIEW_INSTALL_STEAMOS'); (modal.querySelector('button.primary') as HTMLButtonElement).click(); @@ -36,6 +37,7 @@ async function show(value:any,overrides:any={}) { modal=await show(info(camera)); if(!modal.querySelector('.install-run-user.is-root')||!modal.textContent?.includes('请确认你信任此插件'))throw Error('root warning missing'); if(modal.querySelector('.install-run-user')!.getBoundingClientRect().height>130&&innerWidth>700)throw Error('root warning occupies too much space'); + if(!modal.textContent?.includes('2048 MiB'))throw Error('custom memory limit missing'); console.log('FRAMELY_PREVIEW_INSTALL_ROOT'); modal=await show(info(plugin,[camera])); if(!modal.querySelector('.install-root-dependencies')||!modal.querySelector('.run-user-badge.is-root'))throw Error('root dependency hidden'); diff --git a/tools/build-branding.mjs b/tools/build-branding.mjs new file mode 100644 index 0000000..27284c1 --- /dev/null +++ b/tools/build-branding.mjs @@ -0,0 +1,46 @@ +// Generate platform artwork from the approved SVGs. Commit these outputs so +// native/installer builds do not need Node.js or an image tool at build time. +import {readFile, writeFile, mkdir} from 'node:fs/promises'; +import {fileURLToPath} from 'node:url'; +import {Resvg} from '@resvg/resvg-js'; + +const root = new URL('../', import.meta.url); +const branding = new URL('assets/branding/', root); +const app = await readFile(new URL('framely-app-icon.svg', branding)); +const mark = await readFile(new URL('framely-mark.svg', branding)); +const logo = await readFile(new URL('framely-logo.svg', branding), 'utf8'); +await writeFile(new URL('framely-logo-light.svg', branding), logo.replace('color="#17191c"', 'color="#93c5ed"')); +const sizes = [16, 24, 32, 48, 64, 128, 256, 512, 1024]; +const images = new Map(); +await mkdir(new URL('icons/', branding), {recursive:true}); +for (const size of sizes) { + const png = new Resvg(app, {fitTo:{mode:'width', value:size}}).render().asPng(); + images.set(size, png); + await writeFile(new URL(`icons/${size}.png`, branding), png); +} +await writeFile(new URL('framely-app-icon.png', branding), images.get(512)); + +// ICO entries contain PNG images, supported by the targeted Windows versions. +const icoSizes = sizes.filter(size => size <= 256); +const directory = Buffer.alloc(6 + 16 * icoSizes.length); +directory.writeUInt16LE(1, 2); +directory.writeUInt16LE(icoSizes.length, 4); +let offset = directory.length; +for (const [index, size] of icoSizes.entries()) { + const entry = 6 + index * 16; + directory[entry] = directory[entry + 1] = size === 256 ? 0 : size; + directory.writeUInt16LE(1, entry + 4); + directory.writeUInt16LE(32, entry + 6); + directory.writeUInt32LE(images.get(size).length, entry + 8); + directory.writeUInt32LE(offset, entry + 12); + offset += images.get(size).length; +} +await writeFile(new URL('framely.ico', branding), Buffer.concat([directory, ...icoSizes.map(size => images.get(size))])); + +// A small alpha mask is enough for the native pixel-art Dock renderer. This +// keeps the same SVG geometry without adding a runtime SVG/PNG dependency. +const rendered = new Resvg(mark, {fitTo:{mode:'width', value:48}}).render(); +const mask = Array.from({length:48}, (_, y) => ' ' + Array.from({length:48}, (_, x) => rendered.pixels[(y * 48 + x) * 4 + 3]).join(',')).join(',\n'); +await mkdir(new URL('native/', root), {recursive:true}); +await writeFile(new URL('native/brand_mask.h', root), `// Generated by tools/build-branding.mjs from assets/branding/framely-mark.svg.\n#pragma once\nnamespace framely_brand {\ninline constexpr unsigned char mask[48 * 48] = {\n${mask}\n};\n}\n`); +console.log(`Generated platform icons and native mask from ${fileURLToPath(branding)}`); diff --git a/tools/build-ui.mjs b/tools/build-ui.mjs index 815c484..8eb1b7d 100644 --- a/tools/build-ui.mjs +++ b/tools/build-ui.mjs @@ -5,6 +5,7 @@ await build({entryPoints:['ui/src/main.tsx'],bundle:true,minify:true,outfile:'ui await build({entryPoints:['sdk/src/bootstrap.ts'],bundle:true,minify:true,outfile:'ui/dist/assets/plugin-bootstrap.js'}); await copyFile('ui/index.html','ui/dist/index.html'); await cp('ui/locales','ui/dist/assets/locales',{recursive:true}); +await cp('assets/branding','ui/dist/assets/branding',{recursive:true}); await mkdir('examples/showcase/payload',{recursive:true}); await build({entryPoints:['examples/showcase/page.tsx'],bundle:true,minify:true,outfile:'examples/showcase/payload/page.js',define:{'process.env.NODE_ENV':'"production"'}}); await copyFile('examples/showcase/backend.py','examples/showcase/payload/backend.py'); diff --git a/tools/package-installer.py b/tools/package-installer.py index e436e6c..181b6d6 100644 --- a/tools/package-installer.py +++ b/tools/package-installer.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -"""Package the native installer; no external packaging tools needed.""" +"""Package the native installer, including platform application icons.""" import os import pathlib import plistlib import shutil +import subprocess import tarfile import tomllib import zipfile @@ -21,15 +22,30 @@ with __import__('tempfile').TemporaryDirectory() as temp: if platform.startswith('macos'): bundle = stage / 'Framely Installer.app/Contents' (bundle / 'MacOS').mkdir(parents=True) + (bundle / 'Resources').mkdir() + iconset = pathlib.Path(temp) / 'Framely.iconset' + iconset.mkdir() + for size in (16, 32, 128, 256, 512): + for scale in (1, 2): + suffix = '@2x' if scale == 2 else '' + shutil.copy2(root / f'assets/branding/icons/{size * scale}.png', iconset / f'icon_{size}x{size}{suffix}.png') + subprocess.run(['iconutil', '-c', 'icns', '-o', str(bundle / 'Resources/Framely.icns'), str(iconset)], check=True) + shutil.rmtree(iconset) shutil.copy2(binary, bundle / 'MacOS/framely-installer') (bundle / 'MacOS/framely-installer').chmod(0o755) with (bundle / 'Info.plist').open('wb') as output: - plistlib.dump({'CFBundleName': 'Framely Installer', 'CFBundleDisplayName': 'Framely Installer', 'CFBundleIdentifier': 'org.framely.installer', 'CFBundleExecutable': 'framely-installer', 'CFBundlePackageType': 'APPL', 'CFBundleVersion': version, 'CFBundleShortVersionString': version, 'NSHighResolutionCapable': True, 'LSMinimumSystemVersion': '11.0'}, output) + plistlib.dump({'CFBundleName': 'Framely Installer', 'CFBundleDisplayName': 'Framely Installer', 'CFBundleIdentifier': 'org.framely.installer', 'CFBundleExecutable': 'framely-installer', 'CFBundleIconFile': 'Framely.icns', 'CFBundlePackageType': 'APPL', 'CFBundleVersion': version, 'CFBundleShortVersionString': version, 'NSHighResolutionCapable': True, 'LSMinimumSystemVersion': '11.0'}, output) else: shutil.copy2(binary, stage / exe) if not platform.startswith('windows'): (stage / exe).chmod(0o755) + shutil.copy2(root / 'installer/install-desktop-entry.sh', stage / 'install-desktop-entry.sh') + (stage / 'install-desktop-entry.sh').chmod(0o755) + (stage / 'assets/branding').mkdir(parents=True) + for asset in ('framely-logo.svg', 'framely-logo-light.svg', 'framely-app-icon.svg', 'framely-app-icon.png', 'framely.ico'): + shutil.copy2(root / 'assets/branding' / asset, stage / 'assets/branding' / asset) shutil.copy2(root / 'installer/README.md', stage / 'README.md') + shutil.copy2(root / 'installer/README.en.md', stage / 'README.en.md') shutil.copy2(root / 'LICENSE', stage / 'LICENSE') name = f'framely-installer-{version}-{platform}' if platform.startswith('linux'): diff --git a/tools/package-release.sh b/tools/package-release.sh index 3c882bc..18640c4 100755 --- a/tools/package-release.sh +++ b/tools/package-release.sh @@ -11,7 +11,7 @@ npm run build bash tools/build-native.sh "$cef" binary=${CARGO_TARGET_DIR:-target}/release/framely version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1) -build_id="$version-$(find "$binary" target/native ui/dist sdk templates/plugin examples/showcase packaging tools docs README.md README.en.md LICENSE -type f ! -path '*/__pycache__/*' -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -c1-12)" +build_id="$version-$(find "$binary" target/native ui/dist sdk templates/plugin examples/showcase packaging tools docs assets/branding README.md README.en.md LICENSE -type f ! -path '*/__pycache__/*' -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -c1-12)" stage="$base/release/framely-$build_id" [[ ! -e $stage ]] || { echo 'Release already exists.' >&2; exit 1; } mkdir -p "$stage"/{bin,lib/cef,lib/openvr,share/ui,share/licenses,tools} @@ -30,15 +30,19 @@ cp native/vendor/openvr/LICENSE "$stage/share/licenses/openvr.txt" cp native/vendor/json.hpp "$stage/share/licenses/nlohmann-json.hpp" cp packaging/*.sh "$stage/" cp README.md README.en.md "$stage/" +mkdir -p "$stage/assets" +cp -a assets/branding "$stage/assets/" cp LICENSE "$stage/LICENSE" mkdir -p "$stage/tools/source/examples" "$stage/tools/source/tools" "$stage/tools/source/ui" cp package.json package-lock.json tsconfig.json LICENSE "$stage/tools/source/" cp -a sdk "$stage/tools/source/" mkdir -p "$stage/tools/source/templates" cp -a templates/plugin "$stage/tools/source/templates/" +mkdir -p "$stage/tools/source/assets" +cp -a assets/branding "$stage/tools/source/assets/" cp -a examples/showcase "$stage/tools/source/examples/" cp -a ui/src ui/locales ui/index.html ui/package.json "$stage/tools/source/ui/" -cp tools/build-ui.mjs tools/plugin-dev.mjs "$stage/tools/source/tools/" +cp tools/build-ui.mjs tools/build-branding.mjs tools/plugin-dev.mjs "$stage/tools/source/tools/" cp tools/extract-release.py tools/bootstrap.py "$stage/tools/" find "$stage/tools/source" -type d -name __pycache__ -prune -exec rm -rf -- {} + cp -a docs "$stage/share/" diff --git a/ui/index.html b/ui/index.html index add40c7..05c27a0 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1 +1 @@ -Framely
+Framely
diff --git a/ui/locales/en-US.json b/ui/locales/en-US.json index caf8a69..bcad020 100644 --- a/ui/locales/en-US.json +++ b/ui/locales/en-US.json @@ -3,6 +3,7 @@ "locale": "en-US", "name": "English", "messages": { + "后端内存上限:{0} MiB(含子进程)": "Backend memory limit: {0} MiB (including child processes)", "请先阅读并同意使用协议": "Please read and agree to the terms of use first", "查看使用协议": "View terms of use", "使用 Framely 前": "Before using Framely", diff --git a/ui/locales/zh-CN.json b/ui/locales/zh-CN.json index 2763576..09e5933 100644 --- a/ui/locales/zh-CN.json +++ b/ui/locales/zh-CN.json @@ -3,6 +3,7 @@ "locale": "zh-CN", "name": "简体中文", "messages": { + "后端内存上限:{0} MiB(含子进程)": "后端内存上限:{0} MiB(含子进程)", "请先阅读并同意使用协议": "请先阅读并同意使用协议", "查看使用协议": "查看使用协议", "使用 Framely 前": "使用 Framely 前", diff --git a/ui/src/agreement.tsx b/ui/src/agreement.tsx index e34c65a..8254caf 100644 --- a/ui/src/agreement.tsx +++ b/ui/src/agreement.tsx @@ -13,7 +13,7 @@ export function AgreementGate({agreement,refresh,onClose,onRevoked,initialDeclin async function decline(){if(agreement.accepted){setConfirmRevoke(true);return;}setBusy(true);setError('');try{const result=await api('agreement.decline',{view:viewKey()});if(!result.closed)setDeclined(true);}catch(e){setError((e as Error).message);}finally{setBusy(false);}} async function selectLanguage(value:string){setBusy(true);setError('');try{await api('language.save',{language:value});await refresh();}catch(e){setError((e as Error).message);}finally{setBusy(false);}} - const header=
Framelyvoid selectLanguage(value)}/>
; + const header=
void selectLanguage(value)}/>
; if(declined)return
{header}

{t("请先阅读并同意使用协议")}

; return
{header}

{t("使用 Framely 前")}

{t("用户协议")}

{t("Framely 提供插件的集中安装、管理、运行和更新等功能。第三方插件由各自开发者提供,插件的功能、内容及运行行为由插件开发者负责。")}

{t("Framely 不保证第三方插件的安全性、可靠性或兼容性。插件商店收录、下载校验或权限提示不代表对插件安全性的认可或担保。")}

{t("请自行甄别插件来源、安全性及所需权限,谨慎决定是否安装、授权或运行,并备份重要数据。插件可能读取或修改数据、访问网络、调整设备设置;授予较高权限可能影响系统和设备。")}

{t("在适用法律允许的范围内,因第三方插件的安装、授权、运行、更新或卸载造成的数据丢失、隐私泄露、设备故障或其他损失,Framely 及其开发者不承担责任。")}{' '}{t("本协议不排除或限制法律规定不得免除或限制的责任及用户权利。")}

{t("不同意本协议时,请停止使用 Framely。协议发生变更时,需要重新确认。")}

{t("隐私声明")}

{t("Framely 本身不会主动收集、上传或出售用户个人信息,不内置用户行为跟踪或遥测上报。")}

{t("为提供管理功能,Framely 会在设备本地保存插件清单、配置、运行日志、协议同意记录及其他必要状态。访问密码仅保存加盐哈希值。")}

{t("获取插件目录、刷新订阅、下载插件和检查更新时,Framely 会按配置访问相应服务器;使用代理时,请求会经过相应代理服务。这些服务可能获知 IP 地址及请求信息,其数据处理由各自运营者负责。")}

{t("第三方插件可能自行收集、读取或上传用户信息。Framely 不保证插件不会收集信息,也不保证插件的数据处理行为符合你的隐私期望。")}{' '}{t("请查看插件的权限、说明及隐私政策,自行决定是否使用。")}

{error&&

{error}

}
{confirmRevoke?
{t("拒绝协议将停用全部插件")}

{t("插件窗口将关闭。重新同意后,需自行启用插件。")}

:agreement.accepted?<>{t("用户协议与隐私声明")}
:<>
}
; diff --git a/ui/src/icons.tsx b/ui/src/icons.tsx index 9a2ce5e..a052a3c 100644 --- a/ui/src/icons.tsx +++ b/ui/src/icons.tsx @@ -16,4 +16,4 @@ const paths:Record={ refresh:<>, }; export function Icon({name,size=24,filled=false}:{name:IconName;size?:number;filled?:boolean}){return } -export function Brand(){return } +export function Brand({wordmark=false}:{wordmark?:boolean}={}){return {wordmark?'Framely':''}} diff --git a/ui/src/install-review.tsx b/ui/src/install-review.tsx index de4542a..846bacc 100644 --- a/ui/src/install-review.tsx +++ b/ui/src/install-review.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {t} from './i18n'; import {InstallPlan} from './relations'; +import {MemoryLimit} from './memory-limit'; import {PluginAuthor,PluginLinks} from './store'; import {runUser,RunUserNotice} from './run-user'; @@ -24,6 +25,7 @@ export function InstallReview({title,info,busy,error,confirmed,onConfirmChange,o {manifest.details&&manifest.details!==manifest.description&&
{t('完整介绍')}

{manifest.details}

} + {!info.plan&&} {info.runAsChanged&&} {error&&

{error}

} diff --git a/ui/src/main.tsx b/ui/src/main.tsx index b6d600c..618ed68 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -8,10 +8,11 @@ import {NetworkSettings,NetworkPanel} from './network'; import {Subscriptions,Subscription} from './subscriptions'; import {RelationsInfo} from './relations'; import {InstallReview} from './install-review'; +import {MemoryLimit} from './memory-limit'; import {installScrollbars} from '../../sdk/src/scrollbars'; installScrollbars(); import React,{useEffect,useRef,useState} from 'react';import{createRoot}from'react-dom/client';import{uploadChunk,api,viewKey}from'./api';import {Icon,Brand} from './icons';import {Store,Sources,PluginImage,PluginLinks,PluginAuthor,installedIcon,runJob,Transfer,Job,Presentation,compareVersions} from './store';import {SystemUpdates} from './updates';import './style.css';if(location.pathname==='/notifications')document.documentElement.classList.add('notification-view'); -type Manifest=Presentation&{id:string;name:string;version:string;author:string;description:string;backend?:{runAs:string;autostart:boolean};lifecycle?:{runAs?:string};dependencies?:Record;optionalDependencies?:Record;conflicts?:Record;exclusiveResources?:string[];ui:{quickPage?:string;windows:Record}}; +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};notifications:any[];runtime?:Record;running:string[]}; @@ -98,7 +99,7 @@ function App(){ if(reviewAgreement)return setReviewAgreement(false)} onRevoked={closed=>{setAgreementDeclined(!closed);setReviewAgreement(false);setPage(null);setManage(null);}}/>; if(parts[1]==='notifications')return ; if(windowPlugin){const p=status.database.plugins[windowPlugin];return
{p?.manifest.name??t("插件窗口")}
{p?.enabled&&!status.database.safeMode?:

{t("插件已停用或正在启动。")}

}{error&&

{error}

}
;} - return
Framely{managerMode?<>:}
{error&&

{error}

}{status.database.safeMode&&

{t("安全模式:第三方插件暂停运行。")}

} + return
{managerMode?<>:}
{error&&

{error}

}{status.database.safeMode&&

{t("安全模式:第三方插件暂停运行。")}

} {selected?<>
{selected.manifest.name}{managerMode&&}
{selected.enabled&&!status.database.safeMode&&selected.manifest.ui.quickPage?:

{t("插件已停用、处于安全模式或没有快捷页面。")}

}:
{(tab==='favorites'||tab==='installed')&&<> {managerMode&&

{t("已安装")}

{plugins.length}{t("个插件,")}{plugins.filter(p=>p.enabled).length}{t("个已启用")}

setQuery(e.target.value)}/>
} @@ -115,7 +116,7 @@ function App(){ {tab==='catalog'&&setTab('sources')} busy={busy}/>}
} {importing&&
{await inspect(p);setImporting(false);}} busy={busy}/>
} - {manage&&

{manage.manifest.name}

· {manage.manifest.version} · {manage.manifest.backend?.runAs??manage.manifest.lifecycle?.runAs??'steamos'}

{status.database.plugins[manage.manifest.id]?.error&&

{status.database.plugins[manage.manifest.id].error}

}
{t("插件依赖")}
{status.database.plugins[manage.manifest.id]?.error?.includes("卸载清理失败")&&}
} + {manage&&

{manage.manifest.name}

· {manage.manifest.version} · {manage.manifest.backend?.runAs??manage.manifest.lifecycle?.runAs??'steamos'}

{status.database.plugins[manage.manifest.id]?.error&&

{status.database.plugins[manage.manifest.id].error}

}
{t("插件依赖")}
{status.database.plugins[manage.manifest.id]?.error?.includes("卸载清理失败")&&}
} {install&&void resolveChoice(id,url)} onCancel={()=>setInstall(null)} onInstall={()=>void action(async()=>{await runJob('install.start',{ticket:install.ticket,approve:true,approveRunAs:confirmRunAs},setTransfer);setInstall(null);setTransfer(null);})}/>} {transfer&&void inspect(downloadRequest):undefined} cancel={()=>void api('job.cancel',{job:transfer.id}).then(()=>setTransfer(v=>v?{...v,phase:'cancelled'}:null))} close={()=>setTransfer(null)}/>} diff --git a/ui/src/memory-limit.tsx b/ui/src/memory-limit.tsx new file mode 100644 index 0000000..fa0c67d --- /dev/null +++ b/ui/src/memory-limit.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import {t} from './i18n'; +export function MemoryLimit({manifest}:{manifest:any}) { + if(!manifest.backend&&!manifest.lifecycle)return null; + return {t('后端内存上限:{0} MiB(含子进程)',{0:manifest.backend?.memoryLimitMiB??512})}; +} diff --git a/ui/src/relations.tsx b/ui/src/relations.tsx index 5460a1b..56e46fd 100644 --- a/ui/src/relations.tsx +++ b/ui/src/relations.tsx @@ -1,6 +1,7 @@ import {t} from './i18n'; import React,{useEffect,useState} from 'react';import {api} from './api';import {Select} from './localized-select'; import {RunUserBadge} from './run-user'; +import {MemoryLimit} from './memory-limit'; export function RelationsInfo({plugin}:{plugin:any}){ const [dependencies,setDependencies]=useState([]),[dependents,setDependents]=useState([]),[error,setError]=useState(''); useEffect(()=>{let live=true;async function load(){try{const [deps,users]=await Promise.all([api('plugin.dependencies',{plugin:plugin.id}),api('plugin.dependents',{plugin:plugin.id})]);if(live){setDependencies(deps);setDependents(users);setError('');}}catch(e){if(live)setError(String(e));}}void load();const timer=setInterval(()=>void load(),2000);return()=>{live=false;clearInterval(timer);};},[plugin.id]); @@ -10,5 +11,5 @@ export function InstallPlan({info,choose}:{info:any;choose:(id:string,url:string const choice=info.choiceRequired;const[value,setValue]=useState('');useEffect(()=>setValue(choice?.candidates?.[0]?.url??''),[choice?.dependency]); if(choice)return

{t("选择依赖来源")}

{choice.dependency}{t("在其他来源中可用,请明确选择。")}