From cbe8a26a19bb72a209cb13aaaa35980caf651195 Mon Sep 17 00:00:00 2001 From: Dexrn ZacAttack Date: Sat, 31 Oct 2026 01:03:26 -0700 Subject: [PATCH] feat: readme update, remove displayname requirement from server, add topbar to device page view, only cache needed info from remote --- README.md | 147 +++++++++++++++++- repo_assets/device_page.png | Bin 0 -> 37518 bytes repo_assets/remote_page.png | Bin 0 -> 64635 bytes src/Audio/UniScan.Audio/UniScan.Audio.csproj | 4 + .../Views/Remote/Device/DevicePageView.axaml | 20 ++- .../Remote/Device/DevicePageViewModel.cs | 5 +- .../Remote/Device/DeviceRootPageViewModel.cs | 4 +- .../Views/Remote/Device/DeviceViewModel.cs | 4 +- .../Views/Remote/RemoteInfoControl.axaml | 2 +- .../Remote/RemoteInfoControlViewModel.cs | 9 +- .../Views/Remote/RemoteLinkControl.axaml | 3 +- .../Views/Remote/RemoteRootPageViewModel.cs | 22 ++- .../Views/Remote/RemoteViewModel.cs | 9 +- .../Config/Remote/RemoteDto.cs | 11 +- .../DI/Factory/RemoteFactory.cs | 4 +- .../Remote/Device/RemoteDevice.cs | 2 + .../Remote/RemoteServer.cs | 4 +- .../Remote/RemoteStorage.cs | 6 +- .../UniScanClient.DependencyInjection.cs | 6 +- .../Data/Info/Remote/RemoteBranding.cs | 18 ++- .../Data/Info/Remote/RemoteInfo.cs | 9 +- .../Data/Info/Remote/RemoteLink.cs | 13 +- .../Data/Info/Remote/RemoteSocial.cs | 5 +- .../Social/RemoteInfoManager.cs | 7 +- .../UniScan.Server.Core/UniScanServer.cs | 29 +--- template/README.md.template | 147 +++++++++++++++++- 26 files changed, 409 insertions(+), 81 deletions(-) create mode 100644 repo_assets/device_page.png create mode 100644 repo_assets/remote_page.png diff --git a/README.md b/README.md index ea4a1bb..c24b804 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,154 @@ ![GitHub Release](https://img.shields.io/github/v/release/UniScanProject/UniScan?sort=date&filter=UniScan.Server.Host%2F*&display_name=tag&label=Server) -Modular scanner/radio client and multi-broadcast server. Plug in a scanner, set it up in the server software, and open +Modular, cross-platform scanner/radio client and multi-broadcast server. Plug in a scanner, set it up in the server software, and open it up to the internet for others to listen to, and control it yourself by logging into an admin user. +Modular meaning that custom built modules will be able to fully extend the client and server via hooks, event listening, configuration stages, and more all without requiring e.g. Harmony hooks for most simple extension needs. + +The client works by connecting to a "Remote", which contains devices that the client can 'subscribe' to receive updates, see current scanning info, and (soon) listen to any device's configured audio track(s). + +Remotes require a plugin that adds support for whatever devices plan to be connected, with device pages being rendered on the serverside and sent to the client. +This allows the client to not require too much special setup to communicate with virtually any Remote. + +In the future, there will be easy ways for hosters to embed the UniScan client in their website with a preset configuration to automatically add and connect to their server(s). + + + + + + +
+ UniScan receiving and displaying data from a remote SDS100 +
+ UniScan receiving and displaying data from a remote SDS100 +
+ UniScan connected to a +
+ UniScan connected to a "remote" that has one device +
+ +### Roadmap +Our full roadmap can be viewed on our [GitHub Project](https://github.com/orgs/UniScanProject/projects/2) + +#### (rough) Feature roadmap +- [X] Module support +- Official Extension Modules + - [ ] Virtual Scanner debug module + - [ ] Transcript module (via some STT model) + +##### Server +- [X] Modular custom scanner support framework +- [X] Networking +- [X] Support for multiple network hosting methods (TCP, Websocket (via module)) +- [X] Serverside rendering of scanner info to send to the client +- [ ] Serverside rendering of server,serverside plugin options to send to the client +- [ ] Serverside rendering of a scanner's display to send to the client +- [ ] Custom serverside rendered pages to send to the Client +- [ ] Scanner audio track streaming + - By audio track, I mean having multiple audio tracks, since I believe some radio devices can have multiple outputs? I don't fully know yet. + - Either way, no reason to not have multiple. +- [ ] Users and Authentication +- [ ] Announcement posting from users +- [ ] Remote scanner management from users with the correct permissions +- [ ] Scanner programming (may not happen) + +##### Client +- [X] Connecting to remotes +- [X] Displaying remote info (name,description,links,logo,announcements,etc) +- [X] Displaying live device info (such as current channel, scan list, etc) +- [ ] Listening to audio tracks +- [ ] Input optimized UI + - i.e. better UI for tv remotes,controllers on Android TV, better UI for narrow touchscreens on Android,iOS. + - But all will be accessible crossplatform anyway +- [ ] Nicer theme +- [ ] User login +- [ ] Rendering of custom serverside rendered pages created by the server +- [ ] Internal server for connecting local scanners without the need for hosting + +## Setup +UniScan currently supports Windows, macOS, Linux, Android, and iOS (kind of) +In addition to these platforms, UniScan can also run [entirely in your browser!](https://uniscan.dexrn.me) + +> [!IMPORTANT] +> Browser support is currently broken due to some OPFS garbage, sorry. +> +> will fix soon hopefully. + +Additionally, Android TV does work, but no optimized UI has been made as of now. + +Below, you can learn how to setup and install UniScan for your platform. +#### Windows +- [Install .NET 10](https://dot.net/download) +- Download the latest Windows release from the releases tab (or via GitHub Actions artifacts) +- Unzip it +- Open UniScan.Client.App.Platform.Desktop.exe + +#### Linux +- [Install .NET 10](https://dot.net/download) (usually via your distro's package manager) +- Download the latest Linux release from the releases tab (or via GitHub Actions artifacts) +- Unzip it +- Open a terminal cd'd to the unzipped folder +- Run `chmod +x ./UniScan.Client.App.Platform.Desktop` +- Now you can open UniScan.Client.App.Platform.Desktop + - A small oversight is that some file managers will associate e.g. Kate with .desktop files despite the file being an exec + - You may need to use the terminal to run UniScan in this case until something is figured out + +> [!NOTE] +> There is no packaging available for Linux yet (not coming until at least the first Alpha release) + +#### MacOS +- [Install .NET 10](https://dot.net/download) +- Download the latest MacOS release from the releases tab (or via GitHub Actions artifacts) + - Note that this is unpackaged due to package testing being very hard as I don't own a Mac +- Unzip it (if using Archive Manager, double click in Finder) +- Open a Terminal +- Unquarantine the folder via `xattr -dr com.apple.quarantine {Path}` +- CD into the path with the already open terminal +- run `chmod +x ./UniScan.Client.App.Platform.Desktop` +- Run `UniScan.Client.App.Platform.Desktop` + +> [!NOTE] +> There is no packaging available for MacOS yet mainly due to the fact that its almost impossible for me to test as I do not own a Mac +> Thanks Apple for locking down everything............ +> +> However, I do intend to make packaging available for all platforms during the alpha or release stage. + +#### Android +- Download the latest Android APK from the releases tab + - Also available in the GitHub Actions artifacts, but you must download the one with "signed" in the name. +- Open the file, then tap Install (in Package Installer) + - It may require you to enable "Install unknown apps" if you have never installed an APK from the app you are opening it from before + +> [!NOTE] +> UniScan is not yet available on F-Droid, but this will come soon. + +##### Android TV +> [!IMPORTANT] +> This specific install guide requires knowledge of ADB, and a device capable of running an ADB client +> +> This guide also assumes you have no other devices currently connected on the client's ADB daemon + +- Download the latest Android APK from the releases tab + - Note that depending on your tv box model and architecture, you may need the `arm` version and NOT the `arm64` version. + - You can figure this out usually by googling for your TV box's model and finding the specs which should list the CPU, then another search to find the architecture of that CPU. + - `arm32`/`armhf`/`arm-v7a` corresponds to the `arm` APK, while `arm64`/`arm-v{8+}a` corresponds to the `arm64` APK +- Enable Developer Mode via clicking the OK button on "Build Number" ~10 times on the About screen in Settings +- Go to the "Developer Options" page in Settings +- Enable "USB Debugging" and "Wireless Debugging" +- Go inside the "Wireless Debugging" page +- Press the OK button on "Pair device with pairing code" +- You should see it show the TV's local IP address, a port number, and a 'pairing code', note these down. (like in your head or something) +- On your client device, run `adb pair {TV IP address}:{TV ADB port} {TV pairing code}` +- Once it pairs, run `adb connect {TV IP address}:{TV ADB port}` +- Now, in the same folder as the UniScan APK you downloaded, run `adb install {APK file path}` + +#### iOS +> [!IMPORTANT] +> It is unknown whether the non-emulator IPA can be installed on sideloading enabled devices. + +**TODO** as I don't have access to a simulator as of writing this. + ## Subprojects | Name | Version | Description | diff --git a/repo_assets/device_page.png b/repo_assets/device_page.png new file mode 100644 index 0000000000000000000000000000000000000000..98a17b47d46492ae842cf98fbed782a0f80191e2 GIT binary patch literal 37518 zcmeFZ1y~&0vM5XhNO0Eyg1ZwKJh%l2!QCM^gF7LDGX!@C!GdRSC+Ohr4ufk3cOKdM zo^$ukzW?{`x#zw2o$von_o7#Ibgc4axNw{xF}-q5Y03_oY_r zT)2lZ7b@?Gi5;J#8-^WmpBmKq-REL5>y3`0M~dE=Hj*74oH$}~A6lYSTAqNjm)@}8 zDz}IaeqWxFkt@dbHvt z9e4E^4HXsD7i}A>6)ko3unsPE+dea{R0?Omz6;I`=pO*~kt0}6h-pl>nyM-{1#kTD z@UR{mAB8*J&)?sX{uAW>!~A-50vD!mY!`kA`JEOH{Rj`t;_Qh~u@84LjU~;RBUfYTYAHt@)D z#+|=7mnr8p#hq|U!?gdDXd*r~$VFzLb`sy+qr?Wc7TVl?8F?n@*K5dhyePQpTww^@ z_MXiSPQPAQ7)xnpprRXG#=UZz=`Splq!`RGeGi?v+$TA}W2$}oQ!T{WDIRp+VSlpX zT9|b`>3&(M(P@gmvETfL(~)JS7;J4h*h^Y`45dH>sCV{y`{M7+wx2r?C!L~8*SqV+ zZP_?izoQTk#9aaONDEhvF5qAjD`UD;@}AIzYnCpE*-~U|iHxlj^)HGkr)e4AtsF## zB+jx+J*w{ug?|}}kyLi}g`Qj&Mx*{zAp-ec0MrvOm>k~gU9>_AmQp^^+9RXBhzvbZ zH3Q7r;Mnmm%YmQC_7^a z45j@yosyZPnsL>tUAAf6e$djV6z-=UG}PDL1xqBoGril(3vgG9vaM-ZX=I^V|Gt-T z(-{{~9G^>}z|P_K;OE#t9Oi<;S$fYa9g8AttmBxdjZ3FV38I80vIvFfhEJYKX)`*N zmYVp=Jj=Y`WB!u&u5d36mugl#DR_`2x_+e)jz+@ABY@@WHIg5?Ne#JG6&7xcDgxDN z!B;&{z*2pd(PwAVcuBG@B?XxAQ!Vo~^n4ziv_Y^KeO1t$2Q}Z40W%klTT-JZtalRL zO^qQ%h#Gp5S*i|T8WvM)h`lWU+kjrOkuf@*sLbl{^i7=^Z!ld*EdSkADiEmVr1(U^(F-lx5+~B{nGu@`A?x4hyZ4(*S`3tE4U-| zLDI;=bWyu&d+|uR`G`>~6F=!YTGU%QXFuI%&n_;97OMr!u0jp^~2*4di#C^g**{I=4w>n~u-ijcR!S$j9=j_!@6BpqfS??(M zR(La!&N4gMYz9{wyG*C-4$U^(ih>wEt22MhNaXYSa-|!DV%VkYe?>tKW%a1xLqK>jeNzb2GFv1y z^dZWh-#oACkO%UUe=SVeO7)v9UFGV25W-_q_6cl5c02aj;N}Oa=Rn9|->diW{NB~` zrNN;50r!#G)^E{gjw~Zzci2B%YtZI-KMGLYE&%Q75&C`Fzf@j2{t}@vOSyOLanTK$ zO`Ik!!0Vb}nqARrjoFgOG9Uguw4UPj~%BdtuW ziLkMPBO{>Fmi3t4l9Fcjn`Y{5IX}AgXz8I;QIPtOAvVr~q;0lw`{^KOCYB5Gesd%N zoyCwy_Q$gW^a9DxrOfuNE9Ci7f$79{zNLd|LJN&8fnM{5#leZV6ys#J${S&)DuTf7 zMk+Av@yYR&O>!{NqheR6S9n%h4B_l`9MuslZ@jMLIm^92G9@q<0xO%<0|{BKA=Lm-YvZ{3xO z%L4?2@9|=QSx`ewje{tgN;illvwcifGUiyb<$Hcb5dm~Wh|62$Obap# zE46B@?NRVF*n8RFEo63Qg^3-N>S^P&d7ZTR@hPJZB|CeyQF1kRen3eVvc!bi>KDP) z;&4-J&nDj=<-ES71zLA8A8SLH51J3XMrz+R#3SAdK=%6@K2v0J+#Vc}(h4*xPr*qmEfY-qit?;FC)$b8Txc79MSKI1*EGQ7kC}nYo>$gxGWr5N^ zy1$lHWeEri9^6n>AHUpK=TziQ>9=3l_}Sm;v%KKFBU34rB7gfpJvL|(r)6g|+FkhcAl!#~j2E@Gu~{5pl-0M5LX8 zkpHYQ0^#cbLFfR)dH3Jdzm5ptZO%gXDsXqVI}ZuPRl$3^T&|V<)U3`n?%89pjFwKd zQ=}-Nw{h>ZWEZ|0L7wPnYa>QYX=zJgWf&tqmCc>m;^I&YMKmU<@7E8(m2SFnU*@8m zmq~2n-#WV*zE#DO>h%wxk&6aX0NObJAl!H6->kQ4aPf)U--Sh?&pia zzW5>Za`uC6Wy?_`2^!GF(CZMJ6WYBg5y9y>DrOUu7B^xrw9z=l@ui~4!E zV_lp@9yDEFr-T3>c_JO6trB-}f3kcw_#!<(=qKVfD4;3-pby|A5gszTGJ;+_@ zv+rKvV2LA%)s%0EDIPr5tyzcq*V3>j`3qei_`XJh&N|fX1r1-$`xk0`|8o~=;y^JC zPV1Cm74lI&SB{Rg#sE?8`T6Pa35oL)=kqQ74aZM)7X`om3LCn&&~2|LSo)fUS!qtN zRI4vgMmG&sXoLoJ{A1Bm2e>c#`{I0DJ%SM;`b?pJr+6)F@OhoeUeinS7ZyI{Zd$o&Ar5-@^_V z(CKRrJ3Ch2hLFCEBQ9l8AN*24f)SH_D6mDH~f!CK-tmf_jzzm<&7>PX(#*)M#}Syg=5(TEgYq@4pL%Z z_Ay$`*pf`XE`{yUY`7I+AYF{M1Tp_6FqR*exczI5PW2S$1XH4$YyxL8nK?~uev(W2 z7YitAO84hlA~OzIBSdY142?TeMp(Ee3zqxKp1{g~3ieGL9=z0X3# zj#`4FE^J?fos!k-P{bY1N!ortrdc?)*T^=}2Mh-eEGVYw2Y8sBX$0vrtrd1Mvbt5V zu=(z4HP;Q(J@4h}AJ`w7H@uug{v~+|?^y==>?cS>5_``+9JRhqF*7`R4jZYC%gviF zP+&tBu6D1X^0H`J1QqI~vnE!jZC6cL*V33XJE)V{k5|%BEUZuvf&x|ZzbRNmTn|3a zQ9J?mz#gS7*ZK+15zfssi7F`>G?9?JoFnY~wMW>w!OhBb3L%fEbBnBAr_XKs=1)>f ze--|Ok-^+#oZ-{REksMT!k^54ROYiVoTER=eI&N_y2kFHMmEMTlW6V}rUN`OO!k?& z&^AZitOVk7e(u}_qSHnWHLNI^%joGEFX7=4vO<-@&l+OI?{p9=QLORDn7*pNx<^2~ zZ16j>tqtX^cHHM>8yeZ&nkM)R!W{}pMx7NLdP#z8QeFpFoGWi7UHI`|NKRs7M+JnK zn7tofz-9{R0?AonLMw4sfQK z3P67%O4kP zb$uw_C={jM@J>r0J4 zD8tR@%D7W})OGYC#pcyz}J0)Ok&3r|YX` zCtNsrhTLhF*=ONcKaphN`PtYzNz!m)_2*7hB1@^G0@qp9B(mVVZ2xQdF<L){5*PnORlZ4ZvQe`>R$I{9nKOhw<-?m+v7XOprKX6bT{=UK6k)oR#;k69fuZLhNxqw_2s*XpwdCv90K{bl~bm2J75%IY1yjhEJJLf1I)9B!}V zS;Y)t|4F3#yr=Ktud4$d(~j5Qoju&VVw027d2KLne^4lc@@F&364>&bHTKdEifvA8 zi`EY(gdJ|D1AfV}H+VTVwEj9&{+UIGU)ujCmoR>74en10ZHBn}XIiEC1E3icq8xaS zJ5tDWI2wP*Ka2r)cX~l;IP|*pv`2?(u~}pvnTQQcwY+`OJMip2Rt06x4!XxJo_!HM_b)kE>8 zPiUXTzWpbG>mb@^ZuPzhRG|L~uu!-)=Rz?(Z ze=6mNdk2Ej%;VNly0I=ohQb>#Y*!RHEnHoqfz1-h(Kb#mI;V=4^lIgn{RFrXubN=+ZQ-U;;Kb>LKR3&-!WMl1tder> zczdK&d96Bm2Cg<;I=?ud2@-+Cfg262o>D5EkH_xL7q+)<&3ty6A1=&)n-y>PO5XdX z)FN7H&BdKrnDC-O>AzMcnXJd{1hMR4=qJ6~;W}y)WnxTl z@oi43kX>g%fyA!b{YB&d;kuWqUWARvdn-CERN=i||jrjrc?2FE&n?8TBtvvocZ z$DN%!AwS}_7acxgwc;E0U)04-cN|l^1SbKw`7NCKRuj89V@&CwKM|s2F{aQY54siS zYi&8`2&uR5#VITbm9e=vS-3Pd!u{rh$OWZGL%eXQkJn=7E}-1GZ-7$}`X|6=%pGhP zy1sx8oK3mNS#v5MT<|M8%CGz(3Xb@QC9czlKOlEf*`-Udi1f=M?0GQkeEJm~wu^3Z z3Ljs}Jn{;@c!l+10`I{S$_Dtm?Q(ldcA9VqgfBi!Ss~&?vgj3VYeoZIhRDSE#cb)K z-$B+wh>XltSmTws)iW>*=qIK{Y|Da zKBzr-9#GHk=Dd54fIH0t2I>DC_5Tvl|DQND5CBZ>oB+Wa+%hV!qG_^*zb<9kj(yq0 zmW!sz|NNQ#LCBY{^Y)Y0mAj^%5m4B&BhlfRc-1Fbx#;Yzk1Ifbf4zLQvPkpTT&pfY zn9!feg}nwfbp7+0mS1AvfBy8247_-s!@W<)O{icaJi`(fQxv=NsNe{6cJ5X9zPLbR zc{_vQ|LmT{yMICI|9SrZHCs67gqv1l$RoTpe)i-w{)XW~?bpzFtU&y4ix;nECsFV9 zvG{;S`@6`Wb?pDd82D2j?%xgmT>`KcVAJk|*!>Vec*ybl5cwc#uK+o{U;oHRFj?9$&U`Q1Q4 zs6S+Kg!7W{Kx!CRLf5jsF#veq47s3+Rakb--IYg5tlk7U4fx;u^r+MggE=++?Na@Z z9Y8-?u&TW8qPzYikK&VMXy5&2J?*U{)RD@;}&RF?~g5H*wX+I7g(!C&6@eW~QqO|Rc%cv*gwLKA4k^2Go6 zmcHp=a>M4<>txjZ=7u1~G?uf6$UK}&nm8+5rIW!l7KGnZlj^Vqvu=SX<;~W+J6veK zZ!zK^LPL6=->TQ(0Wm?W)p4Agm`gB4{U&w-dq%jsV$)o8F@|FQ#fzL)%~l-{DQZR~0&T7kuyj_K=zE z(XGtPOxG7$_`psjL^q~yssU*zyJ|*x4+Zh3^1mxd#kn)}L{@*`67kz8$TtE1PbN{8;^5Tz^;v&T~5m&C*q~X=uFos<(K+jjMelLlCudi4FkUcBkH zzB;YHxuf+~4%E=q#eQeSe|&DQQ%QU{+Wn>(FHX}|i?~SLsQ94`zE?{k} zU!}N#Myzdm#rK_^BiwV?WpJliFJ<~&KQg#e#p+j?n`8YdOVYQd2&l|Fty0w-_r7m^ z+O?SCA*)jIWBta)TfgKPwLYcxcN;6Uv_RgVtAy+m;gi8X;n{^O`-NM>+=P+ch z5y>us%J6AWSf489CPP;)+euJ~*@L$YgC^eDN)qBqN_s$2xkqo4A-kEc9}OSlP$Z}0 z7n${)Ni*=>358_o{YY`%HRr4W3x2H$J7Cu@=A53{+~sHFLVrPO7fD>n)TP)FQ_We| z99V}mHi1DM{#ZN<;d2$hb+G|PHHLd`mTxgzL+GEPI= zsAD}NsOGJHxsQr?$!q^%JXk-ogX-knvnce;Sqh_7aheh)K%}N)le3L0!i2!yoO45vF zJF`AtDqCTe7T7^<1llr1`H(dTWqGpU^-*7tIb!F)? zR&t0~-0rbU1S^9I47ie2>RG#R_@;i*BJrHvoLd-wouO-Mv*gu^toR5;ejpNC_8IB4PHu#A3$q zH?2&Fal3dJ$eOg(eY_ER*-UK5N%up{vwr?88Jo>{Zdz&p7u~)oL_R%6BymJ5y@Jb7 z`T}=(cWz}{?ANjAfCgSRP8brGYV@fwav`f$p$6QEtN~lCMGsEhIv%{WhkwH)Xe;`f z%m>#2@(vgnEfTYYOh_Amjt&^V%`f{$C^df1!8kk>#C=E+``|6;a4)$Xo6RH5;`G&F z*EtR&SBWXcc4&%!vya$;keh7dxznC4_e?quw+^}|bHa@+m77?op8Dqye9~u!%#=Q4 zMui`JE0c6kp%NN{JArfB%0`b77}wJR%8W$Lx7J1<^t@ll0QCs@rx@ZyAa;)uXB0kJ#AcmjnrH#-e|}Sk&o9iO zQb&!zP&+u1B)$b*v+|SUxe;zp4}XZT`|ZlFBu21tz8k+jJ};1yZ!WGLFC+mE&B)ka zzckfSd2tVSn=3%GzH?(*Q8{FRJDFA;J#F-0f<|Ajb;}>D7D#`hlUZl`o-=!BAku~+M`Qu)tgNK{6@>~p!P;*I-kol zhT6L!RrI=)1bw}XVb4Bk<&TS7p=LHj^+%v_N%y4&Hw7DA)P9fs26KVqp zh*UfeCFurj#l3z{fbgb#bXcnzKUViKC$o${`v;tFU5f(Dvlb&0Sn?`L7HY03Op{F6 zMI{%+_4#h73@@-kg>Y~#SAGyN^S%yQ^r|PklORVJipQ|LVZ-;4!YbdIqG(^j&vEf> ze3O!T+UT0BD32|R6cBEpq&jDAtE zPXp!aYD8X%nz@Tu49#h%J;AYvZWGpjuNr2!CM|5{E^I&a_KPiDZ2p@cd#_B!yWwsr z2ZVHMT+lOff(&=f@~b<=8NsJ(%+H=>G|jP}vfu1{XZou}dj{4-&z$ZUayO&L*_vY_ zWvJ7LYo;r$s<3#XGdPESIOSpCW{+DL*Vm7yAj*cCIXrHsN(UWTT)QQiCojINY7pXO zBaEL@%;L5UI)2l$`*gGPo{^kV$CBYU)r?!+yV1S~pC`C<)Qzb?teRf6RMzwM#RZ?h z%0(WNWmvk5iQT?2N84%PRqMH_FIlgrC!hZ6_nZd>!YFiY6+qW*7{6X40$ty%OnH3I z@ArS^n|NW|R>K9p`;VMECq4q_6Ui1q{5O)=Bp__W%t6z0+Htodnf9|=*RyZWxDS-? zLlf450;GSZ-~YT|^bfwt`1io@zXRL{kH!ZW5~($#$Y9aCTe5}^w7N+;(44k1{_YBz z##|F60anrj-j!S){K>Xp$z3)(4AWT;gI(9JPigsyOVf^$4f@5IkgRl)h25m6bHlLv zOsIJ~ddEN5o3V%W8RyLyr#ySgrGZi2T}L`UKI61kg}JDiWN5*)Ux+E1PJD;#d$E?_ zeBC}#tZQl5&Vkl;U;Ly`&dvsxo9}eX%({0Ak8%%!O%*7hRg5daSj4q!i!XK3lyKEq z0iMk9b~<&=#huT2%T=!4N>?sjRvzYvuY0iY7tCEW@SE~^vS5am&qun&HO{uPG#XYS zl&G6?p23%fh&=~(jEMs8%T2gAV38Oq?2n=|^VD3ZrIEO_ui}ea%-|6oq9zJN^IiTH zsyi5B=d5*1qZgIaOKUF1EEQ$Vum=7p-nNo9m@d5I)-pl2aO#J)BQ#h%fnzRLr&jRx zGJrSvT8hyfNF>14@7GvcUhAGRe&K$BiQ?>!`IAWwTZ0e4CSAaLs+`kHQ^H)q!A&$^ z1kKzdFgmF(yG!(%GlkS8Zbvt&o0gt0uS8hv3>4Z)hdHlM4x8HbYb$At2Jc2#l#Z3m zS%8}7+8B~P@kQ6eFUYr-7a(8DDGzSvgj2(RWAW4!73AX%q+_Oa-Ko645@3a*w}!*Z z(eq(MKKPzN#Ca}=EiD4>#!>y1HW;@f5^Mc;FJhkf+U>qZ05se$a9ys-5Ax zbzbSHY!t$)sI_gvyoStWVm`M$`HL_gCf>w&hdIK;D`;Y0N?+=p*~r(ZJ7*0%lw%j=*y@}eH$Jkhv_6OlyCdjb z{K`Nk5s)}Hh#Bz^;hXP+D-=a37O-In+cUvLH&=ea_8`@`TF1URH$k)yi2`^bY4OQ^ zWv*l!hKdBp!Y#NlsC1L*1`@RWS6Niw2Ietf;4QTCg!OGGeSmq=A4iv)4psp)TSiZg*hS zcTRHouw?C%-z=#xCPH@FQIe%c-LQ_bBf7RnBx;&Hpq92*L42^x$*WmZpjP+#+x(e# zxpcVqDc$F`1b|2zqZ$b|ET+E9uoZ2nfnG=lw4)Xa+H5zqNLs5%<#1T^sGyGw%&RW9OnWD_nZD zr1m=1GCEE_sL~=J6x-$X&d|{ttid!}>h!;D#m*sni|5Ioh+^n>?{BnPK6qg_yIfrr zB9U%X<|g&Bw8!La%c&``?G49rNLQIsMkk6+76C$W`t6A+M~-SxN;8TG}qZ+)L6 z@9untUG9Px+gOfO&5tTb2_K+u?ZZ|7`B<52kWnF=vvHW-rJ(Fa&ts$?VQ83i%IdY! z(d}9Lv^&@)3aK|)Da(zqGEoipRc)d$+z-f!fnYSvb}U5GAuRsz?R=-8%Ub)6kIzFR z5N36+xNn+5hI}5_(sG}F@H@mSOL>!~>?$iie}f=l+Q1_j$A)x7Q5hxK5NB715sqwp zX!yoMWQj{7n*C7Xgq!nsL-`g9>K+N>fY2^+iCvO8xWu~dfjDsG?hFIT7ujC!0Uixu zP(X2#KJ&ev-h>H+^ocmo_@f@z&9!3Tr?t(B>v(K9iY3tg4b8&YqHX3s>J+2+4wKdDmB3~xj`Duz$e>4zl zT5pg})On?weIv2-P@T$Vhjd zzB8T(<6+b9KKvSg-G(d^ja8-j?cgxdj|O&ffN;U|%Z23qOkUKfD>oFOqp?8sLf4?l z++qvl(~mOQohl;@8IJIjq;1)uJL8*5 zqA>xmjgS%_GkDVLoz0sq;2=fvr4&aq%@nbxbz;l8&-Z+GbM{xAop{d?umq}RW#u2i zFX>KI*^FbvM3z^OcLbrygROc%)QCpQi<<8gI5v6jymBh9ZcDk^h_U~Yh!K4bP#z#B z8bdft`gCSgB2pk25smP{F3-r0eu; z$jf$$Zt)|ey?z}QdqYf#u8-vztI+n+TmSrJ&KMQtmZ=JN$xZJ^voBX&8P>A&{OkOj z?4D#NE0Yrs$g(0GZQEC6B?~X1`|J4AlV#z(&O7Sx;o`P;JU1{RjC0P5roM|-Yv$4R zYa2lgh0lwdJcKei7X9_$XJU$vfPiHA-F!@zv4x_rF}B)sg9u@w=3^xqr=PRM*{Q3R zm%rsib7lAe#FG7Dr~V-&7g{f%II>Untt z1ctbs>8c7GF*wr+!a^2HRfe?E10! zRQWx5TXxJ}Phpelrp}fwf&=Jcs8nAkPRQzg!S;A7cJVIY@m~CbqJ9MFBPq7*$PT(i zz@&>Q&^eqN`j>+cyCt~UwQ#N#Nk?e+DaK+)K5K=Tav>c`(oyxSnSm^6yp7zaunnh` zxOMZ_mRuG2%!JO>qVDmsY27cXapdK$g;Z;VW~=S3I8D7iXc*oVBzA{trfA5m59h?e zQ{1O!jK``)U-;<1v<&MBcQE;VlKI+Jtv+{QhrjqmipEp)XxE}%PWYx7wdHrWZe1oA zc7O}r&LE9P9g5u%ymAq+BK_NKGxSAXucM?#lOXai z_TL?u!2T30=psG%cjNcXlk8VB@(*Qb|ACu9WY9bF4Gf8tnmZua|L*oa{VB(yIjM@a zqUiN8TlZ6rIK4#?Yw$5?ja%&?waqvs4yavk+goV8?_pxle(ABu%`tgy^84c@0{V|COvBb3#ub4 zrOJ#cd5h()UgwTKhLQMmH&h}$>E@o(EHpRiC^uK@h`=bw4Rt&}=Sn=7B;ens2*%dd z_1SYk*%eUKeb#9-3#0G4AfSP2fl94gRQbgKV+@a|88Uu$y*JNZ!*vbc7oSlOX|XVk9d!=5YgX zA-mF27ZJfWd)rPVrBcmA;W;5%x;D@U^nAD{6p_WZ-+zetB){Bm_u$rGWw6u`6*;o#>}qR}!ID zOc!O9XSOU065@CB^g2&5>3mb2q8Of@Ku1N-E-y(_aWTW!o7oavQONCRq-yb1FtM(> zvJx`iBtlAzf7HwN{l$!}G+IUov!rDB&`{HvYtzX0&7<>4 zZAwdcIJ4KXyTdXej8sgXHoeTx0rf({tv)P{UeUot{ppQ@eRR^3POqh?l%Kz zLJHY^W|pd%%PRSARDZq)FHC3*^qX5wBnqmozQL#UV!>ht`pC@i^N;ZH+3WZ0Ll$&WaizT)5Uot{X zE_rELRtGASG8#YMXv}2p4Kgjf>Oww|rsb!-Uuv{QRaT8^ih`_^4vLro-OkWvBRmzO z@+?u-8l3uG*G8VE_?nAb%jTPPb4ly{GF4P1AJwfacviJ+9i}1i@LI zX8!WjJh!bX^)1X)FKltKD_>JJy2tT3!BOjK$ige_^7`8dFB>Tl{-|!HjEHb%g{zv;M)j?zd3**<|T^lK5=%<5{ZwT?7?5r`U$vztMn~lo z7Ge;T>`AM?1*WT>s>DQ&$>_uO%yI|>5TZaHdA{$eG-W8;@9ksWLb>+>B2yUYP9tq3 zCGA0mrAMtEyGANlxGOVT$LYiQ=~_dJ^ubCF1Y41%|+tq@Ok2JokCm}L=Mf>eP;G+iA{1r z7rj$XxuoPvWtM5v7)*4tKIGMmhz}F}1Q=%cEgLp`p)&FU*u_ue9WFOVWmh;tgjjipi_dp~XlG+dAkd)JRqXA(kv4zo;7xUi$DMX}2YYa6#l z)TvBvzA)$&(k!qYkv@7zy#ZFl#04ZZyF2Lh)N>QBSZ@hu^`-!;PU+jy zq+P|IpW(9V?rl~unL>`cJKeHoVOoj(Pz-{;Ea51=Hq3N6jE3ST{q<_ix2eHEf8pvuK&@l zl8vWkv^PqF1iWFro_FTmp)a;9BGLSLa;L7TY`#%i_dAFt-L6D>5e<55#RHn+D3yAb zh(%_-9#=7J#$3%z?83Fp-#@qbMu|VF$9Lk~omy;;UZ7HMS2;`rT;JXfc018$^z||h z855ep^`ySfYzTtpiWHxm1T{{~Y^D#7*GG#L0>Ugek_Aa?tJ>+I9 zfHOM9i?`R1=*_5ZEn6C4E;q@}$E(9h$+)JkEw|U7Xo@T;30_3&LBoXzRVdqd~@+B_V^Y50)TXx?>wz{{CZ1i z3zK|ZjY%XI3)>D%m$B@p1}F#c6PAHmP4S5e zziT0OGI3}u*SzXVS5rj8!Pc+m(N8OJ7KUhH7G9e(_|gg;`&iy+FDBUa# z=EG3Vw6x|{me=!|2=6}*qqEj#yZ(Vqs9dL4oul&X|b#BTI$INXS*l;+^fk)2g_*G9e6 zF86j17Hj#s8)>OtCMwEMoOgItJ2K5!nd$n|f0)-bUOxw0fc4M(CB#m;i+C(TngA7p ztGLLh0-Os?9-Ee4lyQB9K1P;PlP%H497pBj#7@Pv=f0gx4K5ZVD|l1pl6Qi6H#Z(` z6`y(5nUmXB`NA^yl?KF-IQvrRGP)wj({zsaNVMm44Sgl zqtqGBRUeo)c;K>@*oD7|=_Ls4<)WhE+TL#Op38j4Khft|eS%cCOF(53e(B#I1=(^1@B;e znt5ZjVCItfJ1xYA(BZk7r}*>ueH}n==>~sT<~Wn;%*x;a2l$S5usCrvZ*6muVhSDQ+F9_&TZYK(rLXUUR`@(zD2uaay-PLk#B+$$S<8yFh-z%L78Bg=6Y(I zBVfwnbDm}HEVQyZgSMw@7^n}OJ!V`RiF9ZFN#~8(L+~9duxU0KDB@I2_~N%fRrJ}? zIsHf2i(_=mwCLCVh)w}|&j`EzUil+99q|6V!}Mnhl*bupf3nSgw$%UsK7)CaK<`Wr zz!Irl`@pw%UT&Yr%&NKc(c;C>MuexIh&^%A1!2Bq#7-EQT40NAE1AMGJy_}1v->9J zEoaOmIHzA>K}k;tR=}JzS*oYubVAWH3J+|i5tLC)()QhjY2cOe5pI>(MkPJRGeHje-x^t)EFkn%x`JCztAyi`4 zv(u-L)r*$d_gQ)U&AU;JoAXFGcSqGzNw)*a4AZ@4g=S^5^}5|fy*$l}&9t#K>m9C= z#&;XKI(}4f^pST7Y`RBj@oEZNF-h{hFQ^~w-%yvFgwus}hedGy6usD|JTf85(ktA< z1{v7?NnODN8dadNx0m;omUOdvS3?S$kG!O$?#;>r`Ygueg@+A@>glp; zSx}{qCWqBvDlDK|pFX!r7P`$++UKXDGT~1*EvO!!p~A)y7F4{ zck~^n!KPF*#IdJS<6&Z35>#K7B~~`+SiN(3D;&PLY2ytRoLE^nUt@to0LUf*#nQ`K zJo7xqT)_SOiOVT(&n-UH&`KKYVlr$h@)QH}Nwx}JXxgd6N+P2n^;JB%IUSq+*V5x` z;OTj%tVkNG18VTq+rPW9zl}zi37y5n&x58c#>z2Q?BsIZI+1w1s=9KMS*Hdd*Qx3hHjJO25_F0z7Zu^ z7mdMfmX8z3c%{6to`^DxL*aC`AR9!7x^4^gU|Hfqf!p{j)nhK#jNTTn*LYB+n)yUM zl9=1mTk**KqdWV1XR!j^kZIZP^%DUty%{If6WInxu-;$5L1+XDDecsG2m5xd; z4LP?BO)dBV$dY@(z%qk=K#bE_NELE>z2*=Y=_(QGjaiUl<2Bf$ zYV7d6kTb4ET{^!3O0p>r^bL3zki{?U9KKG}&zTIf%;Pr@Bnd%3yJjVpC#a_=ocd1W zUMPSqtd%%fxE*m_dsHSZd9yG@Y7egH(TfWXCi0^m6k?$Dk7wWo6~&d@g#|b9Z+?qX z@nTO*)PHCp+`9ry_M?2(Wc%(sb)BCIT%^ebU8&X2*;TLbKRj4?52 zS1ZOz7D^Xvk? z!6`YAMkvR-m!E-0uR1JA;tfhKF|0j9*_K>Ix1`)$AXK!);ki+j|CIjH0$X5L2{QZgeZPpnEms6mp09hzI z<{6|}w+w!4E*~l-TNEFvr?vHEQ&sz8>8bVte@o3d6qhWGU6&r>rrlP^tYsZ zoR;|M#rp>br7#DCVpip%u1yhTQ$^$84ujT#4tpG*HjewnM#0buVr!mYup_i4V0!;k z3@VBt6%4@-YiU}DNOK{Owzo%Y77eQ&&W~FkXy+KKwwABUlVY@dbnGtmzY-G_=1NHY zc7tulr7-tCgFfQ7KyV8;TW&2(zKe)2&+`gnbp^_(p-PP9Qts>J?2Y9k&KBUh2huYr z$rfwB{3x#3`V|unxH;ks`I}7TDyc3{c_RI=iv7BoKZvMl_0n8(eq zGH!37(t5t^hK8=ATkW=idxiqDR-4(@8|%gVeJ3@@xMEv$-P_~hh|4a5(=nfHh3p#! z={J(CHCp8>t&5CR&kJtX%SKxSpGAnz2rJLjA6Vbc_(r|1IvXZ33CnV2xlCA#M?76% zw6N@PiDjIz0T1W?)CW%X;Hyh>u67Vcl^l(J>sMT$vWnwZ5CUVJmPq0YDI*+}SlR9H4r$Bdc}ORr62jHOu5Zb1cP zX1CvBOVp&ISeV{xSJrB^xSDmf6nZ`F)HNE-ypV=uePRFXS&W*w&OOD;{^RtO`PHSd z0xYIT^?PiGyN|RCNUSpCW+JcQrfCk<$u$6^!z{n@ z`%dSiIRD1_(JqAvKr;_3=NS0lMF@}8B}eM^VGWD1+u~IU%9~W->N&~3`Z`vs#N7R? z8L%xNLs1N%O_#dA+NU-rTB;)h0nI$#QfpKV%Is1KKD!HIt(7&YWW??DJ!C!d<~`0E zY{DGwn%74ai8>-O@1o2Lxw|y;A~l=ZLV{zk>e{ix!(bm04{0-o?GZTcl)W^L0 zusJI9Lw%{#m5fx+Gc}P|Z@1p_;O%U=u%SKjq|DSvG)+f8m0}W~$Ka^3vX?3~t<2(` z_hp+dp9r^t9lq2Zi14Ejp&v(t~`*?6OZum$bDmA$v) z8kYc$_+#Y@I*6{6OvyFeEkYal*RJ$#`j$cC^a!t|=_u=louN_143&w4zFjP&KS_nWn|+8j4~CDou=nW>?QMEeL#OQ-1so|OqRWnCVVqYqU;)hy;W zZUG#fcVIp0^Fi^^uKIUuSrk8?IlAiANAZoqmrnpf22gR}4&j2Or1)fvWtvfeJT$|w zq5d3sO{O_S3n`9jQ$t^%NY^sB^iX&&a6h*{MAc`CZ_usc za~#B28OEJ$Z4B-B(ikih&mm^gTvQ8Rt!7TsLoQ=KbvpGdtl!ZOXK(`@JaFp2Cx{K& zf5#MSClJ#@0JGkk#aiVSAj_JI72d{nPH48}?u0l~wMBJ#vc;6Ktbx3)O{>XOJn8cS z7KVQRRo>9&^$a`PA_Cd0o)#{OeSdnkx{ zl7e7)+Ou8Gu~SWg49}n%d89FX z>4O(cYhy169ew=^OuSdAAvqC3oM^v|Z|WBvP~$c^#pNJJFp*^TDYd}qB62g1dO}Bd zPf7Tamn|T&T2@}y(zogp{LG7UE2bP3c|$uAVK4C(Itgj z07Tw~E?&Z+%x}x#Vh*k%{p2yT-Xk1^x1vsf4wN9$ydOF#Xz7WmDQKzY+9ZA{kx+op z1`JD$z+De5R#AeVZeXhA`o=tdmgd#o8C^$dpLZV%V;woczUdCbYxXbXp`}$I(=>23 zF9ojgGq1eXm69ujqmWIWgd+4Ljpp9qi)HhY{LZuWi=Qg&`^vSrS(LwyYGwNxzsd&P z-n>z5b?Z2U?;Q((!U}DBE z-~r`~%4s9Bd_x@U!3=xjYsoBeqE>`rrdFomc8BrCA{tG0_RQ*jA2la?Yso-`5o*^h z2aSq`P$3-(_Ef8y0Qytr*BEbH=cbXN=vkhr9&t-Z6Dg8@^Foo2ChIP=pJ1o-oZ`Lm zglzeJkk}Whw_-gBY2j-A^Xew*-#B=Idd8#2#csH*s!aGpMLcUl?!|TlFVT0>%-stZ z3ZOF^XMN<5-7+S#bgx)O8293(-VxDYvJAK=PH|hz9FKh?z$$-ocWBz1Ioo1peG_{2b6%!!iNR+6pkN z-~n^1YoVFLxt`ielY-Nek?~VDufCiebdxkQ7Ef{uFT~5VCqiU4%91h+#OF}w*n{t5 za_al#X8Njkg%{ZCda@E68}?bP;!m3S)L}iE-W8}L?oCTSowm}DcT>6jrp5C?XiImd zx`Nh@w%BH#?(Wk2+tJjK3zreE<8tGjXC2t%qBx6mSg9ZSm~bXoG+D<&dZ#9#25xf7 zH3FPV`D1J(gGU>Dq4#0ELN)^SZYTHKcz<&I2*?-(K$6)<#rBu0MpUNj+IRs9mp_JO znPF#XgRM9C+fWISmDSgVP{OCj8ZR;^37S*o+ja)d3;%lJ?e4P7Qlg}-`c!Oeu3iAc zHxWuZv0;c*AQCJ9>5x|ZVebEzEJdGe zS2hiZ6{fr-UvJGIfSo+s%Wpq_?}mAh>)J}Cz8@oY)E6Xqsq01U*zoXfj6Z}F-*AHD z{6#j$ehu^AmQ+hYOA?IQscDyqIk_L39CdzHM7jUo$I_ww8Vp=1 zpn0UOPDc{c;6O08vA5|d_`;mJ=y9Bk%;|>Tu^9^KBWq9dYqAuz3ri{mc8wFwV-VWS zeyMzv?7_B9oEJb}l6Hg7x~(H#FJ^oS_PHJIOMAN7yno5TA(Gw#Ig_781-GC&WA9CB zZ|;{U>=G8}Q%BKKn&8+MSx$rkTB1>sTeRunBsFF=_POLTwJ2TQFW+F-jR@q5K{jA`K0rI=!IcOJ&=zV^AH)^QUW^k9zMt>l!*p$GwFM<_|SPB)sI2b_J9 zB~z8CtJ>CI4{Z|CMn$0Sc`nVMGgN#bH_=qHKKj0e#p#MTdrx~Q(tSL6BLamx0ctOT z(X7vzLJ(d>j~?*4M`8W6_GD;kb2gUCM@(XyuGuS|yZOOYkjWUYVMDr59t0rW0=!ui zrK&(!8Xhj)s9$E*EQSE?7260rD7>=ai6Af!fI5J)yoTwmQdaL5SPS#M(kIrm+Jh;U zPSptNb#S)%)Qa4c6(v=Re6{7)t(BgU%2!%@eS7KAsAHk;3!%ZaVXv={a8?iit21D) zo_?Ilvv4gM(G=~!O|;3t>V}(W`{H$;!-dLBnD5-Y+6fS{l7?OX!;QsUh31ITg*QJz zl``MOt=hygNUw_igcq%wnd9iy;?g+$%gbaZq?KN3dAtuv+oQ79uO z3eqHkO?GYvTK>%bhjYWO%6Xr(SMn_&;7{kof1k_YpK(k4t;|>F#B!(&*$Fc?3SRE0 zp0KDM{=j#FS=D~1+4BRP)TsF((Ee!ftB*_>279yjP)g0l0Gv=frQBu6RfadHT`ssESVPE5}_wJ>O=&Rnw|E zQdW6|O*>*mF; zA*K7!%OS*Ff}dl#!GRGeo-F$fD9L&6$brn{_WI7~GJgNgGtRpjVP*DAnBph3XMui_ zEX#NaA`J-y=-f-2)Q@fglS0R!M%onxPI^-%^%fy*lu73W`3y-XDym;o1Vh>mC+uV! zwyo5e4WOg7=~V;k!V%%5>qhjK@UKgn@UF1_t{gLIgM`oCs*?*k5gA$ka@*(Jg^>4w zCseE=3c>}3Boop^abhH*4v1g&V6JsQsPBXoztVHg&r$+yS5-8jE3Ng;6znseSN+@7%U^Hsr-J&A z(oQ5|ek!PcLizleHh(IpzphXI_7=V>r~zs>{@?(zh$T(M`T0qrG}S2ZS%u0xUgV`g zPkN<$?{Hl1(TFf2T86Zm?!&(>1^FHTj=>IbgWAbxLsb32pfG#S`=?s z!S!uVMD3xG3%lrn(miwxwi$5pdqhx43Q|@35n6>W6P~aX{2?ugy$vj~-)$3DM|m2x zFDjDwK5@?%VJ!wGpy!O=I9Icz4aJf!g#H0$G;V4;DMLa`kAVUUOlL&@f@ahJ1p;qT z)^UXZpOnt_YrhUWYG{8M`ixg}kkWw-_$~l&n*zi#ewpQ9lyDVh48*0oc7pH(eoP{y zt$23O#ST;kFn|jD^{hwdY{kjq!U;&NgwYj3Se(m`iH~d6y?Ruf*pDq6hvyCc0bTYl zE}DO@ULgk%XJ^s&XCyAcWRnJvE9_mNlB{b!&Fg>sQ61vf+}5;eJ6HA{OzFQCQtdqzl!0#sti2%ZF6oc^Z9!=vSZ!%wE`o9 zh(vxTH?{=_=sZc4-_zm^Ik z(iGo|L`i+nH?Zv!vPE=jXBNsBgp^U*pC9H$X<3s_6u&N<213(aDNH4o?k(5Em#j&B zkM4u@1^N-B(G$y9=R2fb$yRBs&Y}IO^670M^8bQOK_VTr<4#(P7_0rwVIS0v^?J{z ztlv%$q*yatsoCv}pDooSX=tNu4j1@ZVE6MBZ?e=$?P15t(F!U5GifQ%P`vy<*Mk1iScc`xxD z#w7G+#pskUJ?1z_a-SVWvA0yka0Mo%mX2uh?fIr`bN*2LEG8(nUBhMLUZZEvuUmE| zz?UZ4v&~l>I&?V8yrgqZN36FOd?`DkQ#y&u-}A|sj9AIU zcGvbtP(ilUX@eszoAnL94imDju_BdcZO6JgPaHxDh9IZ5Nv|1e@V`)WEgx2s9ag*L zJYQ-O(X{M_Pp*n^7flGeBO3%>54Ql-Sq};o8R?~qZ5f-)F(1aGlzNJ7K9e?t^zFSG z?NP+RA)C5|v>2j&3yTUlnh)$C=&uHRrvCiaEH#vaw#_m35gc`(A5Qe>Sz$O#QOuMs ze4^QZf!;Z-%0K~aUMAl_zvp)ybU_U|wjf3n6cR{e;^6Qy16`-t;^Wxb+TY0Uuk4oi zfvcx5SA|pc!SZ)}BEr1YSi0gzmoJFp6nu@Q-3s8?^;#t}dsi2^^Uen3T9CRy8~9rn z!5`W0%Ix%jdXSuux=k^ZXaDs~4%a8%ptr z9P(VcZd4cl!p?|J<#kqE?wY00c~` zxQ&!qeN;b|flG0vemblXX)Q-0gK}PfR!oKT;IFL&g{@HH!8mV>9x@0esWJGyRmrX| zz?b^AU1_V#hh_jZKrQI|+4GPg`d^auV}ugP$=cKDisr(oKqTXa?5;dmu1MxV;Qs*b CSmS{J literal 0 HcmV?d00001 diff --git a/repo_assets/remote_page.png b/repo_assets/remote_page.png new file mode 100644 index 0000000000000000000000000000000000000000..e1d6113883670d7ee2748a0dc7ce5146258dfde7 GIT binary patch literal 64635 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 ev`7JSw?4mvZ;1*?2^R6|c>jN^5$x~) literal 0 HcmV?d00001 diff --git a/src/Audio/UniScan.Audio/UniScan.Audio.csproj b/src/Audio/UniScan.Audio/UniScan.Audio.csproj index 576f4f4..24f36dd 100644 --- a/src/Audio/UniScan.Audio/UniScan.Audio.csproj +++ b/src/Audio/UniScan.Audio/UniScan.Audio.csproj @@ -8,4 +8,8 @@ AnyCPU + + + + diff --git a/src/Client/UniScan.Client.App/Views/Remote/Device/DevicePageView.axaml b/src/Client/UniScan.Client.App/Views/Remote/Device/DevicePageView.axaml index c3a27c6..bc664de 100644 --- a/src/Client/UniScan.Client.App/Views/Remote/Device/DevicePageView.axaml +++ b/src/Client/UniScan.Client.App/Views/Remote/Device/DevicePageView.axaml @@ -7,5 +7,23 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="UniScan.Client.App.Views.Remote.Device.DevicePageView" x:DataType="device:DevicePageViewModel"> - + + + + + + + + + diff --git a/src/Client/UniScan.Client.App/Views/Remote/Device/DevicePageViewModel.cs b/src/Client/UniScan.Client.App/Views/Remote/Device/DevicePageViewModel.cs index 0a34f62..c470a80 100644 --- a/src/Client/UniScan.Client.App/Views/Remote/Device/DevicePageViewModel.cs +++ b/src/Client/UniScan.Client.App/Views/Remote/Device/DevicePageViewModel.cs @@ -14,10 +14,13 @@ public class DevicePageViewModel : ViewModelBase public DeviceViewModel DeviceViewModel { get; } public SSRSlotContext SSRViewModel { get; } + + public IRemoteRootPageDeviceNavigatorProxy RemotePageNavigator { get; } - public DevicePageViewModel(DeviceViewModel deviceViewModel, IUISlotRegistry registry) + public DevicePageViewModel(DeviceViewModel deviceViewModel, IUISlotRegistry registry, IRemoteRootPageDeviceNavigatorProxy remoteNavigator) { DeviceViewModel = deviceViewModel; + RemotePageNavigator = remoteNavigator; Identifier id = new("UniScan", "ssr", "slot", "device", DeviceViewModel.Device.Identifier); if (registry.TryGet(id, out SSRSlotContext? vm))//stupid hack because for some reason it keeps creating new SSRViewModel or something weird diff --git a/src/Client/UniScan.Client.App/Views/Remote/Device/DeviceRootPageViewModel.cs b/src/Client/UniScan.Client.App/Views/Remote/Device/DeviceRootPageViewModel.cs index 3bd47bc..1ab2c24 100644 --- a/src/Client/UniScan.Client.App/Views/Remote/Device/DeviceRootPageViewModel.cs +++ b/src/Client/UniScan.Client.App/Views/Remote/Device/DeviceRootPageViewModel.cs @@ -13,11 +13,11 @@ public class DeviceRootPageViewModel : SubPagedViewModelBase, IDisposable private DevicePageViewModel _mainPage; - public DeviceRootPageViewModel(DeviceViewModel deviceViewModel, IUISlotRegistry registry) : base(new EmptyPageViewModel(), UniScanApp.Identifier.Derived("view_model", "device", deviceViewModel.Device.Identifier)) + public DeviceRootPageViewModel(DeviceViewModel deviceViewModel, IUISlotRegistry registry, IRemoteRootPageDeviceNavigatorProxy remoteNavigator) : base(new EmptyPageViewModel(), UniScanApp.Identifier.Derived("view_model", "device", deviceViewModel.Device.Identifier)) { DeviceViewModel = deviceViewModel; - _mainPage = new DevicePageViewModel(deviceViewModel, registry); + _mainPage = new DevicePageViewModel(deviceViewModel, registry, remoteNavigator); CurrentSubpage = _mainPage; } diff --git a/src/Client/UniScan.Client.App/Views/Remote/Device/DeviceViewModel.cs b/src/Client/UniScan.Client.App/Views/Remote/Device/DeviceViewModel.cs index 100a633..a4c420c 100644 --- a/src/Client/UniScan.Client.App/Views/Remote/Device/DeviceViewModel.cs +++ b/src/Client/UniScan.Client.App/Views/Remote/Device/DeviceViewModel.cs @@ -15,10 +15,10 @@ public class DeviceViewModel : ViewModelBase, IDisposable public DeviceRootPageViewModel RootPageViewModel { get; } - public DeviceViewModel(RemoteDevice device, IUIBindingsRegistry bindingsRegistry, IUISlotRegistry slotRegistry) + public DeviceViewModel(RemoteDevice device, IUIBindingsRegistry bindingsRegistry, IUISlotRegistry slotRegistry, IRemoteRootPageDeviceNavigatorProxy remoteNavigator) { Device = device; - RootPageViewModel = new DeviceRootPageViewModel(this, slotRegistry); + RootPageViewModel = new DeviceRootPageViewModel(this, slotRegistry, remoteNavigator); bindingsRegistry.RegisterDict(Device.States).AddTo(_disposables); } diff --git a/src/Client/UniScan.Client.App/Views/Remote/RemoteInfoControl.axaml b/src/Client/UniScan.Client.App/Views/Remote/RemoteInfoControl.axaml index a6dac73..9978db8 100644 --- a/src/Client/UniScan.Client.App/Views/Remote/RemoteInfoControl.axaml +++ b/src/Client/UniScan.Client.App/Views/Remote/RemoteInfoControl.axaml @@ -15,7 +15,7 @@ - + diff --git a/src/Client/UniScan.Client.App/Views/Remote/RemoteInfoControlViewModel.cs b/src/Client/UniScan.Client.App/Views/Remote/RemoteInfoControlViewModel.cs index 331550f..bb56ad7 100644 --- a/src/Client/UniScan.Client.App/Views/Remote/RemoteInfoControlViewModel.cs +++ b/src/Client/UniScan.Client.App/Views/Remote/RemoteInfoControlViewModel.cs @@ -1,14 +1,17 @@ using System.Collections.Generic; using System.Linq; +using R3; using UniScan.Client.App.Views.ViewModel; using UniScan.Network.Data.Info.Remote; namespace UniScan.Client.App.Views.Remote; -public class RemoteInfoControlViewModel(RemoteInfo info) : ViewModelBase +//TODO this is done in a stupid way but rushing to get this done asap +public class RemoteInfoControlViewModel(RemoteInfo info, string fallbackDisplayName) : ViewModelBase { public RemoteInfo Info { get; } = info; - + public string DisplayName => !string.IsNullOrWhiteSpace(Info.DisplayName) ? Info.DisplayName : fallbackDisplayName; + public List Links { get; set; } = - [.. info.Branding.Links.Select(l => new RemoteLinkControlViewModel(l))]; + [.. info.Branding.Links.Where(l => l != default).Select(l => new RemoteLinkControlViewModel(l))]; } \ No newline at end of file diff --git a/src/Client/UniScan.Client.App/Views/Remote/RemoteLinkControl.axaml b/src/Client/UniScan.Client.App/Views/Remote/RemoteLinkControl.axaml index b0634fd..0178753 100644 --- a/src/Client/UniScan.Client.App/Views/Remote/RemoteLinkControl.axaml +++ b/src/Client/UniScan.Client.App/Views/Remote/RemoteLinkControl.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" xmlns:remote="clr-namespace:UniScan.Client.App.Views.Remote" + xmlns:properties="clr-namespace:UniScan.Client.App.UI.Properties" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="UniScan.Client.App.Views.Remote.RemoteLinkControl" x:DataType="remote:RemoteLinkControlViewModel"> @@ -12,7 +13,7 @@ - + diff --git a/src/Client/UniScan.Client.App/Views/Remote/RemoteRootPageViewModel.cs b/src/Client/UniScan.Client.App/Views/Remote/RemoteRootPageViewModel.cs index 82608a4..89203e7 100644 --- a/src/Client/UniScan.Client.App/Views/Remote/RemoteRootPageViewModel.cs +++ b/src/Client/UniScan.Client.App/Views/Remote/RemoteRootPageViewModel.cs @@ -4,6 +4,7 @@ using System.Net.Mime; using System.Threading.Tasks; using Avalonia.Threading; +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; using ObservableCollections; @@ -11,7 +12,6 @@ using Serilog; using Shiki.Common.Identity.Slug; using Shiki.Common.Identity.Slug.Formatting.Formatters; -using Shiki.Internationalization; using Shiki.Internationalization.Component; using UniScan.Client.App.UI.ServersideRendering; using UniScan.Client.App.Views.Global; @@ -35,17 +35,17 @@ public class RemoteRootPageViewModel : SubPagedViewModelBase, IDisposable, IRemo private RemotePageViewModel? _mainPage; private readonly CompositeDisposable _disposables = new(); - - private readonly TextComponentResolverService _textComponentResolver; - public RemoteRootPageViewModel(RemoteViewModel remoteViewModel, TextComponentResolverService resolver) : base(new NotConnectedRemotePageViewModel(remoteViewModel), UniScanApp.Identifier.Derived("view_model", "remote", new Slug(remoteViewModel.Remote.Id.ToString()))) + public IRelayCommand NavigateCommand { get; } + + public RemoteRootPageViewModel(RemoteViewModel remoteViewModel) : base(new NotConnectedRemotePageViewModel(remoteViewModel), UniScanApp.Identifier.Derived("view_model", "remote", new Slug(remoteViewModel.Remote.Id.ToString()))) { - _textComponentResolver = resolver; _notConnectedPage = (NotConnectedRemotePageViewModel)CurrentSubpage; RemoteViewModel = remoteViewModel; RemoteViewModel.Remote.ConnectionStatus.AsObservable().Skip(1).Subscribe(OnConnectionStateChanged).AddTo(_disposables); + NavigateCommand = new RelayCommand(((IRemoteRootPageDeviceNavigatorProxy)this).Navigate); } private void TearDown() @@ -60,8 +60,14 @@ public void Dispose() TearDown(); } - void IRemoteRootPageDeviceNavigatorProxy.Navigate(RemoteDevice device) + void IRemoteRootPageDeviceNavigatorProxy.Navigate(RemoteDevice? device) { + if (device == null) + { + Dispatcher.UIThread.Post(() => { CurrentSubpage = (ObservableObject?)_mainPage ?? _notConnectedPage; }); + return; + } + DeviceViewModel? p = RemoteViewModel.DevicesView.FirstOrDefault(page => page.Device.Identifier == device.Identifier); if (p != null) { @@ -139,5 +145,7 @@ private void OnConnectionStateChanged(IConnectionStatusContext ctx) public interface IRemoteRootPageDeviceNavigatorProxy { - void Navigate(RemoteDevice device); + IRelayCommand NavigateCommand { get; } + + void Navigate(RemoteDevice? device); } \ No newline at end of file diff --git a/src/Client/UniScan.Client.App/Views/Remote/RemoteViewModel.cs b/src/Client/UniScan.Client.App/Views/Remote/RemoteViewModel.cs index 56b7acc..265e946 100644 --- a/src/Client/UniScan.Client.App/Views/Remote/RemoteViewModel.cs +++ b/src/Client/UniScan.Client.App/Views/Remote/RemoteViewModel.cs @@ -9,7 +9,6 @@ using R3; using Shiki.Common.Identity.Slug; using Shiki.Common.Identity.Slug.Formatting.Formatters; -using Shiki.Internationalization; using Shiki.Internationalization.Component; using UniScan.Client.App.Core.Helpers; using UniScan.Client.App.UI.ServersideRendering; @@ -53,17 +52,17 @@ public RemoteViewModel(RemoteServer remote, IServiceProvider provider) Pipeline = new RemoteConnectionPipeline(); _serviceProvider = provider; - RootPageViewModel = new RemoteRootPageViewModel(this, provider.GetRequiredService()); + RootPageViewModel = new RemoteRootPageViewModel(this); IUIBindingsRegistry bindingsRegistry = provider.GetRequiredService(); IUISlotRegistry slotRegistry = provider.GetRequiredService(); - _deviceList = new SynchronizedViewMirror, RemoteDevice>, DeviceViewModel>(Remote.Devices.CreateView(kvp => new DeviceViewModel(kvp.Value, bindingsRegistry, slotRegistry))); + _deviceList = new SynchronizedViewMirror, RemoteDevice>, DeviceViewModel>(Remote.Devices.CreateView(kvp => new DeviceViewModel(kvp.Value, bindingsRegistry, slotRegistry, RootPageViewModel))); DevicesView = _deviceList.Output; - Remote.RemoteInfo.AsObservable().Subscribe((v) => + Remote.RemoteInfo.AsObservable().Subscribe(v => { - RemoteInfoControlViewModel? n = v != null ? new RemoteInfoControlViewModel(v) : null; + RemoteInfoControlViewModel? n = v != null ? new RemoteInfoControlViewModel(v, Remote.DisplayName.Value) : null; InfoViewModelStream.OnNext(n); OnPropertyChanged(nameof(InfoViewModel)); diff --git a/src/Client/UniScan.Client.Core/Config/Remote/RemoteDto.cs b/src/Client/UniScan.Client.Core/Config/Remote/RemoteDto.cs index 5737180..c2270fe 100644 --- a/src/Client/UniScan.Client.Core/Config/Remote/RemoteDto.cs +++ b/src/Client/UniScan.Client.Core/Config/Remote/RemoteDto.cs @@ -6,11 +6,14 @@ namespace UniScan.Client.Core.Config.Remote; [method: JsonConstructor] -public record RemoteCacheDto( - [property: JsonPropertyName("remote_info")] - RemoteInfo? RemoteInfo) +public record CachedRemoteInfoDto( + [property: JsonPropertyName("display_name")] string? DisplayName, + [property: JsonPropertyName("logo_url")] Uri? LogoUrl) { - public static RemoteCacheDto FromRemoteServer(RemoteServer remoteServer) => new(remoteServer.RemoteInfo.Value); + public static CachedRemoteInfoDto? FromRemoteServer(RemoteServer remoteServer) => remoteServer.RemoteInfo.Value != null ? new CachedRemoteInfoDto(remoteServer.RemoteInfo.Value.DisplayName, remoteServer.RemoteInfo.Value.Branding.LogoUrl) : null; + + //TODO replace + public RemoteInfo ToSimpleRemoteInfo() => new(DisplayName, null, new RemoteBranding(LogoUrl, []), new RemoteSocial()); } [method: JsonConstructor] diff --git a/src/Client/UniScan.Client.Core/DI/Factory/RemoteFactory.cs b/src/Client/UniScan.Client.Core/DI/Factory/RemoteFactory.cs index c8bd5d0..cff1245 100644 --- a/src/Client/UniScan.Client.Core/DI/Factory/RemoteFactory.cs +++ b/src/Client/UniScan.Client.Core/DI/Factory/RemoteFactory.cs @@ -14,11 +14,11 @@ public interface IRemoteFactory { public RemoteServer Create(Guid id, IRemoteConnectionMethod connectionMethod); - public RemoteServer Create(Guid id, RemoteDto dto, RemoteCacheDto? cache); + public RemoteServer Create(Guid id, RemoteDto dto, CachedRemoteInfoDto? cache); } public class RemoteFactory(IServiceProvider provider) : IRemoteFactory { public RemoteServer Create(Guid id, IRemoteConnectionMethod connectionMethod) => new(id, connectionMethod, provider.GetRequiredService()); - public RemoteServer Create(Guid id, RemoteDto dto, RemoteCacheDto? cache) => new(id, dto, cache, provider.GetRequiredService()); + public RemoteServer Create(Guid id, RemoteDto dto, CachedRemoteInfoDto? cache) => new(id, dto, cache, provider.GetRequiredService()); } \ No newline at end of file diff --git a/src/Client/UniScan.Client.Core/Remote/Device/RemoteDevice.cs b/src/Client/UniScan.Client.Core/Remote/Device/RemoteDevice.cs index ecd4f80..f7c16c8 100644 --- a/src/Client/UniScan.Client.Core/Remote/Device/RemoteDevice.cs +++ b/src/Client/UniScan.Client.Core/Remote/Device/RemoteDevice.cs @@ -17,6 +17,8 @@ public class RemoteDevice(Slug id, string? name, bool connec public Slug Identifier { get; } = id; public string? DisplayName { get; } = name; + public string Name => DisplayName ?? Identifier.Value; + public bool Connected { get; } = connected; public RemoteServer Parent { get; } = parent; diff --git a/src/Client/UniScan.Client.Core/Remote/RemoteServer.cs b/src/Client/UniScan.Client.Core/Remote/RemoteServer.cs index 1a88ae1..f56d496 100644 --- a/src/Client/UniScan.Client.Core/Remote/RemoteServer.cs +++ b/src/Client/UniScan.Client.Core/Remote/RemoteServer.cs @@ -38,7 +38,7 @@ public interface IRemoteServerMutationProxy public class RemoteServer : IRemoteServerMutationProxy { - public IReadOnlyBindableReactiveProperty DisplayName { get; } + public IReadOnlyBindableReactiveProperty DisplayName { get; } public Guid Id { get; } @@ -95,7 +95,7 @@ public RemoteServer(Guid id, IRemoteConnectionMethod connectionMethod, IClientSo _remoteInfo.Value = info; } - public RemoteServer(Guid id, RemoteDto dto, RemoteCacheDto? cache, IClientSocketFactory socketFactory) : this(id, dto.ConnectionMethod, socketFactory, cache?.RemoteInfo) {} + public RemoteServer(Guid id, RemoteDto dto, CachedRemoteInfoDto? cache, IClientSocketFactory socketFactory) : this(id, dto.ConnectionMethod, socketFactory, cache?.ToSimpleRemoteInfo()) {} void IRemoteServerMutationProxy.SetSoftwareInfo(ServerSoftwareInfo info) { diff --git a/src/Client/UniScan.Client.Core/Remote/RemoteStorage.cs b/src/Client/UniScan.Client.Core/Remote/RemoteStorage.cs index d655e47..f345a87 100644 --- a/src/Client/UniScan.Client.Core/Remote/RemoteStorage.cs +++ b/src/Client/UniScan.Client.Core/Remote/RemoteStorage.cs @@ -18,7 +18,7 @@ public class RemoteStorage( IRemoteManager remoteManager, IRemoteFactory remoteFactory, DirectoryKeyValueStorage remoteStorage, - DirectoryKeyValueStorage? cacheStorage + DirectoryKeyValueStorage? cacheStorage ) : IRemoteStorage { private readonly CompositeDisposable _disposables = new(); @@ -50,7 +50,7 @@ public async Task LoadAsync() continue; } - caches.TryGetValue(id.ToString(), out RemoteCacheDto? cache); + caches.TryGetValue(id.ToString(), out CachedRemoteInfoDto? cache); remoteManager.Remotes.Add(remoteFactory.Create(id, dto.Value, cache)); } @@ -84,7 +84,7 @@ private async Task SaveCacheAsync(RemoteServer remote) if (cacheStorage == null) return; - RemoteCacheDto dto = RemoteCacheDto.FromRemoteServer(remote); + CachedRemoteInfoDto dto = CachedRemoteInfoDto.FromRemoteServer(remote); await cacheStorage.SaveAsync(remote.Id.ToString(), dto); } diff --git a/src/Client/UniScan.Client.Core/UniScanClient.DependencyInjection.cs b/src/Client/UniScan.Client.Core/UniScanClient.DependencyInjection.cs index c8255de..81c3387 100644 --- a/src/Client/UniScan.Client.Core/UniScanClient.DependencyInjection.cs +++ b/src/Client/UniScan.Client.Core/UniScanClient.DependencyInjection.cs @@ -65,12 +65,12 @@ public static async Task AddUniScanClientAsync(this IService new JsonStorageSerializer(provider.GetRequiredKeyedService("PolymorphicJsonOptions")) )); - services.AddSingleton>(provider => - new DirectoryKeyValueStorage( + services.AddSingleton>(provider => + new DirectoryKeyValueStorage( Path.Combine(paths.CachePath, "remotes"), directoryManager, fileManager, - new JsonStorageSerializer(provider.GetRequiredKeyedService("PolymorphicJsonOptions")) + new JsonStorageSerializer(provider.GetRequiredKeyedService("PolymorphicJsonOptions")) )); services.AddSingleton(); diff --git a/src/Network/UniScan.Network/Data/Info/Remote/RemoteBranding.cs b/src/Network/UniScan.Network/Data/Info/Remote/RemoteBranding.cs index 3e89fd8..93539b7 100644 --- a/src/Network/UniScan.Network/Data/Info/Remote/RemoteBranding.cs +++ b/src/Network/UniScan.Network/Data/Info/Remote/RemoteBranding.cs @@ -1,9 +1,21 @@ +using System.ComponentModel; +using System.Text.Json.Serialization; using MessagePack; namespace UniScan.Network.Data.Info.Remote; +///

+/// Remote branding settings +/// +/// The remote's logo URL, if present +/// A list of links to external websites [MessagePackObject] public readonly record struct RemoteBranding( - [property: Key(0)] Uri? LogoUrl, - [property: Key(1)] List Links -); \ No newline at end of file + [property: Key(0), JsonPropertyName("logo_url")] + Uri? LogoUrl, + [property: Key(1), JsonPropertyName("links")] + List Links +) +{ + public RemoteBranding() : this(null, []) { } +} \ No newline at end of file diff --git a/src/Network/UniScan.Network/Data/Info/Remote/RemoteInfo.cs b/src/Network/UniScan.Network/Data/Info/Remote/RemoteInfo.cs index 482df9f..71ecb83 100644 --- a/src/Network/UniScan.Network/Data/Info/Remote/RemoteInfo.cs +++ b/src/Network/UniScan.Network/Data/Info/Remote/RemoteInfo.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Text.Json.Serialization; using MessagePack; using Shiki.Common.Identity; using Shiki.Internationalization.Component; @@ -15,8 +16,8 @@ namespace UniScan.Network.Data.Info.Remote; /// Social stuff (announcements,motd) [MessagePackObject] public record RemoteInfo( - [property: Key(0)] string DisplayName, - [property: Key(1)] ITextComponent? Description, - [property: Key(2)] RemoteBranding Branding, - [property: Key(3)] RemoteSocial Social + [property: Key(0), JsonPropertyName("display_name")] string? DisplayName, + [property: Key(1), JsonPropertyName("description")] ITextComponent? Description, + [property: Key(2), JsonPropertyName("branding"), JsonRequired] RemoteBranding Branding, + [property: Key(3), JsonPropertyName("social"), JsonRequired] RemoteSocial Social ); \ No newline at end of file diff --git a/src/Network/UniScan.Network/Data/Info/Remote/RemoteLink.cs b/src/Network/UniScan.Network/Data/Info/Remote/RemoteLink.cs index d4f209e..6ae1677 100644 --- a/src/Network/UniScan.Network/Data/Info/Remote/RemoteLink.cs +++ b/src/Network/UniScan.Network/Data/Info/Remote/RemoteLink.cs @@ -1,11 +1,18 @@ +using System.Text.Json.Serialization; using MessagePack; using Shiki.Internationalization.Component; namespace UniScan.Network.Data.Info.Remote; +/// +/// A link to an external website +/// +/// The icon url, if present +/// The link title +/// The link itself [MessagePackObject] public readonly record struct RemoteLink( - [property: Key(0)] Uri? IconUrl, - [property: Key(1)] ITextComponent Name, - [property: Key(2)] Uri Url + [property: Key(0), JsonPropertyName("icon_url")] Uri? IconUrl, + [property: Key(1), JsonPropertyName("name")] ITextComponent Name, + [property: Key(2), JsonPropertyName("url")] Uri Url ); \ No newline at end of file diff --git a/src/Network/UniScan.Network/Data/Info/Remote/RemoteSocial.cs b/src/Network/UniScan.Network/Data/Info/Remote/RemoteSocial.cs index 9572641..ffb5679 100644 --- a/src/Network/UniScan.Network/Data/Info/Remote/RemoteSocial.cs +++ b/src/Network/UniScan.Network/Data/Info/Remote/RemoteSocial.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using MessagePack; using MessagePack.Formatters; using Shiki.Common.Identity; @@ -10,6 +11,6 @@ namespace UniScan.Network.Data.Info.Remote; [MessagePackObject] public readonly record struct RemoteSocial( - [property: Key(0)] ITextComponent? MessageOfTheDay, - [property: Key(1)] IReadOnlyDictionary, RemoteAnnouncement> Announcements//TODO make this list of IDs, so that we aren't sending such a large packet if there are many announcements + [property: Key(0), JsonPropertyName("motd")] ITextComponent? MessageOfTheDay, + [property: Key(1), JsonPropertyName("announcements")] IReadOnlyDictionary, RemoteAnnouncement> Announcements//TODO make this list of IDs, so that we aren't sending such a large packet if there are many announcements ); \ No newline at end of file diff --git a/src/Server/UniScan.Server.Core/Social/RemoteInfoManager.cs b/src/Server/UniScan.Server.Core/Social/RemoteInfoManager.cs index ff7671c..b6dda98 100644 --- a/src/Server/UniScan.Server.Core/Social/RemoteInfoManager.cs +++ b/src/Server/UniScan.Server.Core/Social/RemoteInfoManager.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Collections.Immutable; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; using ObservableCollections; @@ -29,7 +30,8 @@ public class RemoteInfoManager public StoredObject Metadata { get; } public StoredObject Branding { get; } - public RemoteInfo GetRemoteInfo() => new RemoteInfo(Metadata.Property.Value.ServerName, Metadata.Value.ServerDescription, Branding.Value, new RemoteSocial(Metadata.Value.MessageOfTheDay, Announcements.Value)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public RemoteInfo GetRemoteInfo() => new(Metadata.Property.Value.ServerName, Metadata.Value.ServerDescription, Branding.Value, new RemoteSocial(Metadata.Value.MessageOfTheDay, Announcements.Value)); public RemoteInfoManager(string directory, IPlatformFileManager fileManager, IPlatformDirectoryManager directoryManager, JsonSerializerOptions? serializerOptions) { @@ -64,6 +66,7 @@ public RemoteInfoManager(string directory, IPlatformFileManager fileManager, IPl new StoredObject, RemoteAnnouncement>>([], [v => v.ObserveChanged().Select(_ => v)], m, m); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public async Task LoadAsync() { await Metadata.LoadAsync(); @@ -72,6 +75,6 @@ public async Task LoadAsync() } } -public record SocialMetadata([property: JsonPropertyName("server_name")] string ServerName, +public record SocialMetadata([property: JsonPropertyName("server_name")] string? ServerName, [property: JsonPropertyName("server_description")] ITextComponent? ServerDescription, [property: JsonPropertyName("server_motd")] ITextComponent? MessageOfTheDay); \ No newline at end of file diff --git a/src/Server/UniScan.Server.Core/UniScanServer.cs b/src/Server/UniScan.Server.Core/UniScanServer.cs index 22509c7..3206b4c 100755 --- a/src/Server/UniScan.Server.Core/UniScanServer.cs +++ b/src/Server/UniScan.Server.Core/UniScanServer.cs @@ -1,52 +1,25 @@ -using System.Collections.Immutable; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Schema; +using System.Reflection; using System.Text.Json.Serialization; -using Microsoft.Extensions.DependencyInjection; using Semver; using Serilog; -using Serilog.Core; using Shiki.Common.Extensions; using Shiki.Common.Identity; -using Shiki.Common.Identity.Slug; -using Shiki.Common.Identity.Slug.Formatting.Formatters; -using Shiki.Common.Util; -using Shiki.FileSystem.Implementations.Assembly; -using Shiki.FileSystem.Storage; -using Shiki.FileSystem.Storage.Directory.KeyValue; -using Shiki.FileSystem.Storage.Serializer; -using Shiki.Internationalization; using Shiki.Internationalization.Component; -using Shiki.Internationalization.Component.Resolver; -using Shiki.Internationalization.Translation; -using Shiki.Internationalization.Translation.LanguageSource; using Shiki.ModuleManagement; -using Shiki.TaskPipeline.Context; using Shiki.Settings; using Shiki.Settings.Option; using UniScan.Audio.Server; -using UniScan.Core.State; -using UniScan.Device.Device; -using UniScan.Network; -using UniScan.Network.Data.Info.Remote; using UniScan.Network.Data.Info.Software; using UniScan.Network.Group; using UniScan.Network.Protocol.Packets.Bidirectional.Status; using UniScan.Network.Registry; -using UniScan.Network.Registry.Source.Sources; using UniScan.Network.Server; using UniScan.Network.Server.HostMethod; using UniScan.Network.Socket; -using UniScan.Network.Socket.Configuration; using UniScan.Server.Authentication.Session; using UniScan.Server.Core.Host; using UniScan.Server.Core.Module; -using UniScan.Server.Core.Pipeline; using UniScan.Server.Core.Social; -using UniScan.Server.Core.UserInterface; -using Constants = UniScan.Core.Constants; namespace UniScan.Server.Core; diff --git a/template/README.md.template b/template/README.md.template index 36f89e2..3b16db0 100644 --- a/template/README.md.template +++ b/template/README.md.template @@ -6,9 +6,154 @@ ![GitHub Release](https://img.shields.io/github/v/release/UniScanProject/UniScan?sort=date&filter=UniScan.Server.Host%2F*&display_name=tag&label=Server) -Modular scanner/radio client and multi-broadcast server. Plug in a scanner, set it up in the server software, and open +Modular, cross-platform scanner/radio client and multi-broadcast server. Plug in a scanner, set it up in the server software, and open it up to the internet for others to listen to, and control it yourself by logging into an admin user. +Modular meaning that custom built modules will be able to fully extend the client and server via hooks, event listening, configuration stages, and more all without requiring e.g. Harmony hooks for most simple extension needs. + +The client works by connecting to a "Remote", which contains devices that the client can 'subscribe' to receive updates, see current scanning info, and (soon) listen to any device's configured audio track(s). + +Remotes require a plugin that adds support for whatever devices plan to be connected, with device pages being rendered on the serverside and sent to the client. +This allows the client to not require too much special setup to communicate with virtually any Remote. + +In the future, there will be easy ways for hosters to embed the UniScan client in their website with a preset configuration to automatically add and connect to their server(s). + + + + + + +
+ UniScan receiving and displaying data from a remote SDS100 +
+ UniScan receiving and displaying data from a remote SDS100 +
+ UniScan connected to a +
+ UniScan connected to a "remote" that has one device +
+ +### Roadmap +Our full roadmap can be viewed on our [GitHub Project](https://github.com/orgs/UniScanProject/projects/2) + +#### (rough) Feature roadmap +- [X] Module support +- Official Extension Modules + - [ ] Virtual Scanner debug module + - [ ] Transcript module (via some STT model) + +##### Server +- [X] Modular custom scanner support framework +- [X] Networking +- [X] Support for multiple network hosting methods (TCP, Websocket (via module)) +- [X] Serverside rendering of scanner info to send to the client +- [ ] Serverside rendering of server,serverside plugin options to send to the client +- [ ] Serverside rendering of a scanner's display to send to the client +- [ ] Custom serverside rendered pages to send to the Client +- [ ] Scanner audio track streaming + - By audio track, I mean having multiple audio tracks, since I believe some radio devices can have multiple outputs? I don't fully know yet. + - Either way, no reason to not have multiple. +- [ ] Users and Authentication +- [ ] Announcement posting from users +- [ ] Remote scanner management from users with the correct permissions +- [ ] Scanner programming (may not happen) + +##### Client +- [X] Connecting to remotes +- [X] Displaying remote info (name,description,links,logo,announcements,etc) +- [X] Displaying live device info (such as current channel, scan list, etc) +- [ ] Listening to audio tracks +- [ ] Input optimized UI + - i.e. better UI for tv remotes,controllers on Android TV, better UI for narrow touchscreens on Android,iOS. + - But all will be accessible crossplatform anyway +- [ ] Nicer theme +- [ ] User login +- [ ] Rendering of custom serverside rendered pages created by the server +- [ ] Internal server for connecting local scanners without the need for hosting + +## Setup +UniScan currently supports Windows, macOS, Linux, Android, and iOS (kind of) +In addition to these platforms, UniScan can also run [entirely in your browser!](https://uniscan.dexrn.me) + +> [!IMPORTANT] +> Browser support is currently broken due to some OPFS garbage, sorry. +> +> will fix soon hopefully. + +Additionally, Android TV does work, but no optimized UI has been made as of now. + +Below, you can learn how to setup and install UniScan for your platform. +#### Windows +- [Install .NET 10](https://dot.net/download) +- Download the latest Windows release from the releases tab (or via GitHub Actions artifacts) +- Unzip it +- Open UniScan.Client.App.Platform.Desktop.exe + +#### Linux +- [Install .NET 10](https://dot.net/download) (usually via your distro's package manager) +- Download the latest Linux release from the releases tab (or via GitHub Actions artifacts) +- Unzip it +- Open a terminal cd'd to the unzipped folder +- Run `chmod +x ./UniScan.Client.App.Platform.Desktop` +- Now you can open UniScan.Client.App.Platform.Desktop + - A small oversight is that some file managers will associate e.g. Kate with .desktop files despite the file being an exec + - You may need to use the terminal to run UniScan in this case until something is figured out + +> [!NOTE] +> There is no packaging available for Linux yet (not coming until at least the first Alpha release) + +#### MacOS +- [Install .NET 10](https://dot.net/download) +- Download the latest MacOS release from the releases tab (or via GitHub Actions artifacts) + - Note that this is unpackaged due to package testing being very hard as I don't own a Mac +- Unzip it (if using Archive Manager, double click in Finder) +- Open a Terminal +- Unquarantine the folder via `xattr -dr com.apple.quarantine \{Path\}` +- CD into the path with the already open terminal +- run `chmod +x ./UniScan.Client.App.Platform.Desktop` +- Run `UniScan.Client.App.Platform.Desktop` + +> [!NOTE] +> There is no packaging available for MacOS yet mainly due to the fact that its almost impossible for me to test as I do not own a Mac +> Thanks Apple for locking down everything............ +> +> However, I do intend to make packaging available for all platforms during the alpha or release stage. + +#### Android +- Download the latest Android APK from the releases tab + - Also available in the GitHub Actions artifacts, but you must download the one with "signed" in the name. +- Open the file, then tap Install (in Package Installer) + - It may require you to enable "Install unknown apps" if you have never installed an APK from the app you are opening it from before + +> [!NOTE] +> UniScan is not yet available on F-Droid, but this will come soon. + +##### Android TV +> [!IMPORTANT] +> This specific install guide requires knowledge of ADB, and a device capable of running an ADB client +> +> This guide also assumes you have no other devices currently connected on the client's ADB daemon + +- Download the latest Android APK from the releases tab + - Note that depending on your tv box model and architecture, you may need the `arm` version and NOT the `arm64` version. + - You can figure this out usually by googling for your TV box's model and finding the specs which should list the CPU, then another search to find the architecture of that CPU. + - `arm32`/`armhf`/`arm-v7a` corresponds to the `arm` APK, while `arm64`/`arm-v\{8+\}a` corresponds to the `arm64` APK +- Enable Developer Mode via clicking the OK button on "Build Number" ~10 times on the About screen in Settings +- Go to the "Developer Options" page in Settings +- Enable "USB Debugging" and "Wireless Debugging" +- Go inside the "Wireless Debugging" page +- Press the OK button on "Pair device with pairing code" +- You should see it show the TV's local IP address, a port number, and a 'pairing code', note these down. (like in your head or something) +- On your client device, run `adb pair \{TV IP address\}:\{TV ADB port\} \{TV pairing code\}` +- Once it pairs, run `adb connect \{TV IP address\}:\{TV ADB port\}` +- Now, in the same folder as the UniScan APK you downloaded, run `adb install \{APK file path\}` + +#### iOS +> [!IMPORTANT] +> It is unknown whether the non-emulator IPA can be installed on sideloading enabled devices. + +**TODO** as I don't have access to a simulator as of writing this. + ## Subprojects | Name | Version | Description |