From 73220eb8808c52b882cf6fef4e7cfbfe44fca6ce Mon Sep 17 00:00:00 2001 From: Sandro Kalatozishvili Date: Fri, 20 Oct 2023 02:02:33 +0400 Subject: [PATCH] Implemented custom layout page and button editing menu (#4) * Refactored the codebase, moved common functionality outside the apps * Implemented functionality to load/store buttons for custom layout * Implemented button logic in custom layout * Implemented custom layout edit page * Finished custom layout * Updated exit button behavior on custom page * Updated remote * Updated version and changelog --- README.md | 51 ++++--- application.fam | 2 +- deploy.sh | 35 ++++- docs/README.md | 24 +++- docs/changelog.md | 13 +- screens/custom_layout.png | Bin 0 -> 6441 bytes screens/saved_remote_menu.png | Bin 2784 -> 6532 bytes screens/settings_menu.png | Bin 5864 -> 6123 bytes views/xremote_common_view.c | 48 ++++++- views/xremote_common_view.h | 26 +++- views/xremote_custom_view.c | 201 ++++++++++++++++++++++++-- views/xremote_custom_view.h | 2 +- views/xremote_general_view.c | 47 ++++-- xremote.c | 8 +- xremote.h | 5 +- xremote_app.c | 259 ++++++++++++++++++++++++++++++++-- xremote_app.h | 58 +++++++- xremote_control.c | 74 +++------- xremote_edit.c | 162 +++++++++++++++++++++ xremote_edit.h | 13 ++ xremote_learn.c | 10 +- xremote_settings.c | 60 ++------ xremote_signal.c | 2 + xremote_signal.h | 10 +- 24 files changed, 922 insertions(+), 188 deletions(-) create mode 100644 screens/custom_layout.png create mode 100644 xremote_edit.c create mode 100644 xremote_edit.h diff --git a/README.md b/README.md index 49d8d59..22af9fb 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,30 @@ # flipper-xremote -Advanced IR Remote App for Flipper Device -## About +Advanced IR Remote App for Flipper Device + +## Idea + Navigation to the menu to press each button individually can be often uncomfortable because it requires scrolling to the desired button and selecting it. The idea behind `XRemote` is that all physical buttons are pre-mapped to specific category buttons, and a physical button directly sends an infrared signal. This allows the flipper device to be used as a remote rather than as a tool that has a remote. +## Learn new remote + `XRemote` also introduces a more user-friendly learning approach. Instead of having to manually name each button on the flipper when cloning a remote, the learning tool informs you upfront which buttons it will record. All you need to do is press the corresponding button on your existing remote, eliminating the need to name them individually. +## Custom Layout + +To customize your layout, open the saved remote file, select `Edit` in the menu, and configure which infrared commands should be transmitted when physical buttons are pressed or held. These changes will be stored in the existing remote file, which means that the configuration of custom buttons can be different for all remotes. + + + + + + + + +
Edit custom page buttons
XRemote edit layout
+ +## Standard file support + The application is compatible with standard `.ir` files. However, to ensure functionality, names within these files must align with the predefined naming scheme. If the button is not highlighted when pressed or the notification LED does not light up, the button with the appropriate name cannot be found in the file. Button name | Description @@ -43,21 +62,21 @@ Button name | Description - [x] Learn new remote - [x] Signal analyzer - [x] Use saved remote - - [x] General button page - - [x] Control buttons page - - [x] Navigation buttons page - - [x] Player buttons page - - [ ] Custom buttons page - - [ ] Full button list - - [ ] Rename remote file - - [ ] Delete remote file + - [x] General button page + - [x] Control buttons page + - [x] Navigation buttons page + - [x] Player buttons page + - [x] Custom buttons page + - [x] Edit custom layout + - [ ] Add or remove button + - [ ] All buttons page - [x] Application settings - - [x] GUI to change settings - - [x] Load settings from the file - - [x] Store settings to the file - - [x] Vertical/horizontal views - - [x] IR command repeat count - - [x] Exit button behavior + - [x] GUI to change settings + - [x] Load settings from the file + - [x] Store settings to the file + - [x] Vertical/horizontal views + - [x] IR command repeat count + - [x] Exit button behavior ## Screens diff --git a/application.fam b/application.fam index 0771f12..cf689af 100644 --- a/application.fam +++ b/application.fam @@ -6,7 +6,7 @@ App( requires=["gui", "dialogs", "infrared"], stack_size=3 * 1024, order=1, - fap_version="1.0", + fap_version="1.1", fap_category="Infrared", fap_icon_assets="assets", fap_icon_assets_symbol="xc", diff --git a/deploy.sh b/deploy.sh index 0812f7a..0f1c47f 100755 --- a/deploy.sh +++ b/deploy.sh @@ -2,10 +2,38 @@ # This source is part of "flipper-xremote" project # 2023 - Sandro Kalatozishvili (s.kalatoz@gmail.com) -# Change it according to the root path of the used firmware -FLIPPER_FIRMWARE="/opt/flipper/firmwares/unleashed-firmware" +#FLIPPER_FIRMWARE="/opt/flipper/firmwares/unleashed-firmware" #FLIPPER_FIRMWARE="/opt/flipper/firmwares/flipperzero-firmware" +XCLR_DIM="\x1B[2m" +XCLR_RED="\x1B[31m" +XCLR_RESET="\x1B[0m\n" + +# Parse firmware path from arguments if present +for arg in "$@"; do + if [[ $arg == --firmware=* || $arg == --fw=* ]]; then + FLIPPER_FIRMWARE="${arg#*=}" + fi +done + +# Check if FLIPPER_FIRMWARE variable is set +if [ -z "$FLIPPER_FIRMWARE" ]; then + echo -e "$XCLR_RED""FLIPPER_FIRMWARE variable is not set or is empty. $XCLR_RESET" + echo "You can either export FLIPPER_FIRMWARE variable:" + echo -e "$XCLR_DIM""export FLIPPER_FIRMWARE=/path/to/firmware $XCLR_RESET" + echo "Or pass the firmware path as an argument:" + echo -e "$XCLR_DIM""$0 --fw=/path/to/firmware $XCLR_RESET" + exit 1 +else + echo "Using firmware path: $FLIPPER_FIRMWARE" +fi + +# Check if the path exists and has a applications_user sub directory +if [[ ! -d "$FLIPPER_FIRMWARE" || ! -d "$FLIPPER_FIRMWARE/applications_user" || ! -f "$FLIPPER_FIRMWARE/fbt" ]]; then + echo -e "$XCLR_RED""Firmware path does not exist or does not contain the required flipper context. $XCLR_RESET" + exit 1 +fi + # Private variables XREMOTE_PROJ_PATH=$(dirname $(readlink -f "$0")) XREMOTE_PROJ_NAME=$(basename "$XREMOTE_PROJ_PATH") @@ -28,3 +56,6 @@ for arg in "$@"; do [ $DEPLOY_DONE -eq 1 ] && sudo qflipper fi done + +# Return with success +exit 0 \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 0908f50..7b04d78 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,14 +1,25 @@ -## flipper-xremote -Advanced IR Remote App for Flipper Device +# flipper-xremote -## About -Navigation to the menu to press each button individually can be often uncomfortable because it requires scrolling to the desired button and selecting it. The idea behind XRemote is that all physical buttons are pre-mapped to specific category buttons, and a physical button directly sends an infrared signal. This allows the flipper device to be used as a remote rather than as a tool that has a remote. +Advanced IR Remote App for Flipper Device -XRemote also introduces a more user-friendly learning approach. Instead of having to manually name each button on the flipper when cloning a remote, the learning tool informs you upfront which buttons it will record. All you need to do is press the corresponding button on your existing remote, eliminating the need to name them individually. +## Idea -The application is compatible with standard .ir files. However, to ensure functionality, names within these files must align with the predefined naming scheme. If the button is not highlighted when pressed or the notification LED does not light up, the button with the appropriate name cannot be found in the file. +Navigation to the menu to press each button individually can be often uncomfortable because it requires scrolling to the desired button and selecting it. The idea behind `XRemote` is that all physical buttons are pre-mapped to specific category buttons, and a physical button directly sends an infrared signal. This allows the flipper device to be used as a remote rather than as a tool that has a remote. + +## Learn new remote + +`XRemote` also introduces a more user-friendly learning approach. Instead of having to manually name each button on the flipper when cloning a remote, the learning tool informs you upfront which buttons it will record. All you need to do is press the corresponding button on your existing remote, eliminating the need to name them individually. + +## Custom Layout + +To customize your layout, open the saved remote file, select `Edit` in the menu, and configure which infrared commands should be transmitted when physical buttons are pressed or held. These changes will be stored in the existing remote file, which means that the configuration of custom buttons can be different for all remotes. + +## Standard file support + +The application is compatible with standard `.ir` files. However, to ensure functionality, names within these files must align with the predefined naming scheme. If the button is not highlighted when pressed or the notification LED does not light up, the button with the appropriate name cannot be found in the file. ## Button schema + Button name | Description ------------|------------------- Power | Power @@ -37,4 +48,3 @@ Play_pa | Play/Pause Pause | Pause Play | Play Stop | Stop - diff --git a/docs/changelog.md b/docs/changelog.md index 8a29fa4..79bf05d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,16 @@ +## v1.1 + +Custom layout and bug fixing + +- Implemented custom layout page and button editor +- Updated exit button behavior on general page +- Fixed crash on dev branch firmware builds +- Refactored the codebase and fixed bugs + ## v1.0 First stable release + - Adjusted layout of remote apps - Added learn mode support - Added signal analyzer app @@ -9,7 +19,8 @@ First stable release ## v0.9 First beta release + - Stable saved remote control apps - Flipper standard .ir file support - Horizontal/Vertical view support for all apps -- Settings variable item list and functionality \ No newline at end of file +- Settings variable item list and functionality diff --git a/screens/custom_layout.png b/screens/custom_layout.png new file mode 100644 index 0000000000000000000000000000000000000000..1515e1f1870c3a63531a1113068365b2146be043 GIT binary patch literal 6441 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 VvXGbTE;k6gf&&)&pPwJM=|56+FaQ7m literal 0 HcmV?d00001 diff --git a/screens/saved_remote_menu.png b/screens/saved_remote_menu.png index 06b3b547696e10046e5b1956583c2f39841c0ba2..04fc9e0c3b23794b4eb5f7c7d46014b0e8e265d1 100644 GIT binary patch literal 6532 zcmeHKX;f2Z6TU&fh*45^a3Mrn>XJQy5DB{iRfGyHYO8pY+#6EJLJ}Yt*<28mLR-b9 z0;0A;U64~$K#PE=d@2f5MTJVJ+Co`fP*eo^W+Qmo?|h%(nfD~A zfh&9_SlL=Z5H!Kh*Lx)dVay@O42!h{mK5Swo#5Z)4Z*9FE8!S|OfE%55ePvUD?OPZkJMuo3&)!Y4TRM<)A$n@?rqTqP z-@G2D|Fo6+CG*P)`&-Nzv}vraj#g}wM@TQ*)mHomXYnD8bhTLB&+AEirDX;_ESfj|RdL?r-zNATT75Sp>2@+`af`^yE6~r&%h(a+Oi>>@n)>LBOzj_vG*1yc@s=1*M{MJ??e|Lv)H^U3L(!YeB?E*;&xqo`nF z!6w2>%!Bp^-?b6nAEeAbmGY#e#IC2C+ue-uJa&~?M8SleMFcDTXnR7ZjyZ`XrokyG zS89&e`mMChdDt<>^?dWC*xh}lZq>PKs5UK=QzsvnthzY!B4huj+0R@fw`3xjFSFQa zokgl$X+g=G%cYMBD;RT^Ix+%g*-u->J>E-QIMenCGcx$L^6GRs`B+vlJN&j~$1gj} zsljz2yGvInaPt@O#!ZU)E;q^QXdyoD6NgLQ$uD-!8T`1K^7S(N5R0@~cfPC~ zYKbXEBft3B-|GFO!RbwdGsc>ob3Z3)(o~XhefA>_G;vYo-thIu~U)m$=G5kl-biBLpUyNv4vPs6^3pqN^3bMJ_-&E4`N* zAix`!7^YOpI21}uObj`OL6*ve6q>WMGlfd0(CH-LK~ls@l(32+7n$nBMSlH!?H zk$|InMCg1b6J{YKI?O^zOo5|-#Ad<_63vk~MnD9}ML?Bs#K@|&Pyzr&WAafp>_{W= z5f+uibY#LLXD1em#1^0|I_ks|pa@L|CE#EmQ9+Yvy&6Ukxh4YWYcI=w#n&JM6Liz)MC=8 zWCl}rq8$qdGy{l*wK@d=Iy>lv<0VI6rBog)l}2!h+L8#`$l+suuucM4346mz1b|ZM z3=WOTp*aWBX&eTF!*pIqWpbz^{G|dB8vDPzwaY_r(U06$qyYJ2bte5r1tF38H~njb zNViG^f^I1|Fkjz-0**!mx;g=t{)it2ON0oxJq&U++C_gO1s2W8k;X#=Bo+^KBr(}k zItga-86+o^#dPAKPILs;cQuTzkfO>MSdMrI0gr$yAWt1vgb#Itn*Vk!F=2>y3V<*Y zjY|3}VH88d6z!d1?AV3!7fxJs4kIoZ5U00+%M09vl;O+Jz!|9b-~2Jm;@?a`AdDt? zCw@oi8l~%<7d^-c`Dlk=#${xQ0&OkbxE3HTHg1HLS!OslZ~U$iWF%Y3|{ zA?8@y87K2yt$?737qx$w%!LzY0^?YvpZ}7vtyYuYv!_`vD!T>T7W#R61gjdm z?t91|_~I=B8I_UjP`gdfuE*wv+@E|fc z&KSdfa;5CmKqo6mwiOuN36L2Ek4(@5hqWxi@W-6_FbE*Km7bg zp^fJ$etUk4l|Jamn%|3NA7*T8XzD0)h&RvyuZ`vO> zbg0;^y~rZckQK#V_nZ1=(Yd0Sb=k8FF@<~kKe4-eFQ%((8Z>Nz8C%-cd32BydIu~~ zx54H<%Ikp&Lp{3`YOY<6O}6dJbuV!3wOys2P_X65mhioOr*S8GZd#`?+E0H_prs2C zQ5_d8#C&f5cb$(UVQ{C3{h`9rb#ehxfg+A!&+~xt~AuQ zJSz0J^rgayCyMs~dX#D(#y9nS)oGV?$L3~*?YM(cXkFvMjPtG=z+qCujb?R5RkL=4 z1W;(pQHFVoo94zw1Aaw1f?@~B69*EE87tC3#^#ef(z1as7}(kX@qu`^wfe>?(wjP7 zsnoVD4;;$5dP(402k^O&S}zAel8sp~`&E}cv|tPYpU7sIe^@ccESC;g@m-$`O>@SYK{M2x%~f08>OcM7?a*-88WU&(=6l@5E37Nuy}4(Pjzu%f z%L^inb>(u*q=4a$y6HVX?By!L%F5ch`#bx~8~bY2%*#2U2GKplsoI2I;D7I(e$IviJn&%JO(YN+huK6`h}Fb53t zeA&|`gJUG%F3~WhuPShu1zB&5j=vPzTsTv8it~DLd){o(^YoAtH@~Se4vH$aK6skZ zo`p9JvgF5;NZ9=yccTj$_ATx0$mkB}scz~YM55O7G}eYjWS4C`qw}tJ=hoj@*W10( zC&Qut$%~wQdu`GN;))Cd{PbJy1wD9-7DubLmAD7~Kokg}nvwE(T{JfoUpM3V& zT=>{0J5KGB-P1PY*7Wep=y=A~^KL^1hnDfhcb+|Y!NmElo4wZ}e$XiAi6@b^TlIj> zy;qD4txu+w9Io8;e<=)*`|e@}LW7$`>Bk4}r5=c8w^bSwQzMe`U3DCG@6dq4a78WN z9ZRaJugQ(Nj30P?xId=%dD+lVLxNqL3(5%}8Z7bD-I<9Cuj7*)5LwT!`I~EVQfmf> zoLg&)4BDf5g-y@KRP0O0F>dAb-TUlox*wxfi{ebvuR^gWPM&mo-FL4z;a2IIp4zt7 zYq-s62aNM7eE4F6Yjcie$xD-%V%`4Xh5hezmViq}ye;*0Z_iOp^wEIDcZx%;=iW1F z{79F5RvCXkM)regYUet`Z4RlePpGXl(T+1rB#V>l8lMz}F2iTjyhHmTMuNt;+7uOO z%#`(pO_>LTHu5gf4x-`AnA7J&F~K~ePf1x$MsXg79B<-r-L-#q_p)rvOf^xx60QEd oH6Kl7lJ+sp6fUL3E2ERzyJUM literal 2784 zcmeHJYfw{X8a~7ThZtfk*9b~dc51~7l-T8dDS-x<1woLsXd$yJ$#PL{5|Jz+!pdb( zlu{MsYO%^vK?-rTViIm$rizIyMUV)I(i+*^h=gznN%klS@yGt??Ck8!&g_r#=Q;29 zzTf*i@B5rn5fe?cMr=a>0AL*v9(oV}%-{e313?Q2DS!Wh6avnq5(qI71OhfICzFzz zkpci8Ru@+Lg%8Hqmg@g{sXg9Viv2r|kk?(ZW$3$vtDs#-$K&>rAES~VU&BRYXWJgx zj0&{>7PXIe&-X5(ShQ(q{D+9qut9+YTx%i` z->MF8ma zX|b^Ta@JB`BJBCNGOp(ML2oN~a)#y)wxA5wQF^S%ML?6lLH|vp#Np(0SWojcFW*Ed z==jWHU#H)d`<6%Kz~F&>3T zd%j?=L|qU#69fbW1?lq$jE7G2qe+@V zBooBUOtrRhM%Tu-AF^AoREyic^U!>cls9uKFUNT1_i|DpnJT=!lmu)*rH{~G)6F}2 z*G9Qs9vbBmr)YFyli@xwJlW!(?o&^siRe%9Z9#X$2(uWBsSPw3{8+f;$eg*1ybCsp~~E}4qD zin{*EW-lX;-(ZTP^N&$1(m6Yf<6Op1Y2inVdE1oBV=pWe+zol_8_ew~6p*+*Pc7S? z$7$8;GtPTWtvs?_W#}R2d1EY0_h(7`VZoRO_Ea9(ep#1moY4E1uf`Dto_O*HMjlRM z21HdBaleWfJb?*&M~0Z^zea<*srWx1yW!KlO5MqrdfsAbEZpCP31JZ=qQ`nM-(XSF z&t9^TL}))T1eN5xTsS(hq|+bP7pj!Cb2qgcE7-Z8;Mnw4KfA0I6-fWUQ==C*2>Axa z`jA(@IAH-$bqt!J=j6{Qr5?S5^}D)Mrrm>VgT>+MUJ0o}U932b9n{+Fjz@ZOf{Y_v zC6KJ-)3=FUm}C|!{=*APE&Z=~!~aW}jPKtv*Z(B~O`p+{HH*C!|2Q>X9d!!&83H20 LqC-1F$i@EzXE_Cf diff --git a/screens/settings_menu.png b/screens/settings_menu.png index 6d9d004277ccb76cb6a920d899d6a1ffd5685738..c2f7a75352376775ed14ffa7850aea5bf0030ff9 100644 GIT binary patch delta 2451 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 HpoIScCYtTQ diff --git a/views/xremote_common_view.c b/views/xremote_common_view.c index 3f23051..670bb1d 100644 --- a/views/xremote_common_view.c +++ b/views/xremote_common_view.c @@ -48,6 +48,14 @@ const char* xremote_button_get_name(int index) { return g_buttons[index].name; } +int xremote_button_get_index(const char* name) { + size_t i; + for(i = 0; i < XREMOTE_BUTTON_COUNT; i++) { + if(!strcmp(name, g_buttons[i].name)) return g_buttons[i].index; + } + return -1; +} + struct XRemoteView { XRemoteClearCallback on_clear; XRemoteAppContext* app_ctx; @@ -55,9 +63,14 @@ struct XRemoteView { void* context; }; +XRemoteView* xremote_view_alloc_empty() { + XRemoteView* remote_view = malloc(sizeof(XRemoteView)); + return remote_view; +} + XRemoteView* xremote_view_alloc(void* app_ctx, ViewInputCallback input_cb, ViewDrawCallback draw_cb) { - XRemoteView* remote_view = malloc(sizeof(XRemoteView)); + XRemoteView* remote_view = xremote_view_alloc_empty(); remote_view->app_ctx = app_ctx; remote_view->view = view_alloc(); @@ -89,11 +102,21 @@ void xremote_view_set_context(XRemoteView* rview, void* context, XRemoteClearCal rview->on_clear = on_clear; } +void xremote_view_set_view(XRemoteView* rview, View* view) { + xremote_view_clear_context(rview); + rview->view = view; +} + void* xremote_view_get_context(XRemoteView* rview) { furi_assert(rview); return rview->context; } +void xremote_view_set_app_context(XRemoteView* rview, void* app_ctx) { + furi_assert(rview); + rview->app_ctx = app_ctx; +} + void* xremote_view_get_app_context(XRemoteView* rview) { furi_assert(rview); return rview->app_ctx; @@ -113,8 +136,8 @@ View* xremote_view_get_view(XRemoteView* rview) { InfraredRemoteButton* xremote_view_get_button_by_name(XRemoteView* rview, const char* name) { xremote_app_assert(rview->context, NULL); - InfraredRemote* remote = (InfraredRemote*)rview->context; - return infrared_remote_get_button_by_name(remote, name); + XRemoteAppButtons* buttons = (XRemoteAppButtons*)rview->context; + return infrared_remote_get_button_by_name(buttons->remote, name); } bool xremote_view_press_button(XRemoteView* rview, InfraredRemoteButton* button) { @@ -135,6 +158,23 @@ bool xremote_view_send_ir_msg_by_name(XRemoteView* rview, const char* name) { return (button != NULL) ? xremote_view_press_button(rview, button) : false; } +void xremote_view_model_context_set(XRemoteView* rview, void* model_ctx) { + with_view_model( + xremote_view_get_view(rview), + XRemoteViewModel * model, + { + model->context = model_ctx; + model->up_pressed = false; + model->down_pressed = false; + model->left_pressed = false; + model->right_pressed = false; + model->back_pressed = false; + model->ok_pressed = false; + model->hold = false; + }, + true); +} + void xremote_canvas_draw_icon(Canvas* canvas, uint8_t x, uint8_t y, XRemoteIcon icon) { if(icon == XRemoteIconEnter) { canvas_draw_circle(canvas, x - 2, y, 4); @@ -261,7 +301,7 @@ void xremote_canvas_draw_button_wide( bool pressed, uint8_t x, uint8_t y, - char* text, + const char* text, XRemoteIcon icon) { elements_slightly_rounded_frame(canvas, x + 4, y, 56, 15); diff --git a/views/xremote_common_view.h b/views/xremote_common_view.h index 439b8b3..df0589f 100644 --- a/views/xremote_common_view.h +++ b/views/xremote_common_view.h @@ -21,6 +21,8 @@ #include "../infrared/infrared_remote.h" #define XREMOTE_BUTTON_COUNT 26 +#define XREMOTE_NAME_MAX 16 + #define XREMOTE_COMMAND_POWER "Power" #define XREMOTE_COMMAND_SETUP "Setup" #define XREMOTE_COMMAND_INPUT "Input" @@ -82,13 +84,14 @@ typedef enum { } XRemoteIcon; typedef struct { + void* context; bool ok_pressed; bool back_pressed; bool up_pressed; bool down_pressed; bool left_pressed; bool right_pressed; - void* context; + bool hold; } XRemoteViewModel; typedef enum { @@ -109,17 +112,22 @@ typedef enum { XRemoteViewIRSubmenu, XRemoteViewIRGeneral, XRemoteViewIRControl, + XRemoteViewIRPlayback, XRemoteViewIRNavigation, - XRemoteViewIRPlayer, - XRemoteViewIRCustom + XRemoteViewIRCustomPage, + XRemoteViewIRCustomEditPage, + XRemoteViewIRAllButtons } XRemoteViewID; typedef struct XRemoteView XRemoteView; typedef void (*XRemoteClearCallback)(void* context); typedef void (*XRemoteViewDrawFunction)(Canvas*, XRemoteViewModel*); + typedef XRemoteView* (*XRemoteViewAllocator)(void* app_ctx); +typedef XRemoteView* (*XRemoteViewAllocator2)(void* app_ctx, void* model_ctx); const char* xremote_button_get_name(int index); +int xremote_button_get_index(const char* name); void xremote_canvas_draw_header(Canvas* canvas, ViewOrientation orient, const char* section); void xremote_canvas_draw_exit_footer(Canvas* canvas, ViewOrientation orient, const char* text); @@ -142,7 +150,7 @@ void xremote_canvas_draw_button_wide( bool pressed, uint8_t x, uint8_t y, - char* text, + const char* text, XRemoteIcon icon); void xremote_canvas_draw_button_size( Canvas* canvas, @@ -162,14 +170,20 @@ void xremote_canvas_draw_frame( XRemoteView* xremote_view_alloc(void* app_ctx, ViewInputCallback input_cb, ViewDrawCallback draw_cb); +XRemoteView* xremote_view_alloc_empty(); void xremote_view_free(XRemoteView* rview); InfraredRemoteButton* xremote_view_get_button_by_name(XRemoteView* rview, const char* name); bool xremote_view_press_button(XRemoteView* rview, InfraredRemoteButton* button); bool xremote_view_send_ir_msg_by_name(XRemoteView* rview, const char* name); -void xremote_view_set_context(XRemoteView* rview, void* context, XRemoteClearCallback on_clear); -void* xremote_view_get_context(XRemoteView* rview); +void xremote_view_model_context_set(XRemoteView* rview, void* model_ctx); void xremote_view_clear_context(XRemoteView* rview); + +void xremote_view_set_app_context(XRemoteView* rview, void* app_ctx); +void xremote_view_set_context(XRemoteView* rview, void* context, XRemoteClearCallback on_clear); +void xremote_view_set_view(XRemoteView* rview, View* view); + void* xremote_view_get_app_context(XRemoteView* rview); +void* xremote_view_get_context(XRemoteView* rview); View* xremote_view_get_view(XRemoteView* rview); diff --git a/views/xremote_custom_view.c b/views/xremote_custom_view.c index 46ddaca..423dc03 100644 --- a/views/xremote_custom_view.c +++ b/views/xremote_custom_view.c @@ -9,25 +9,206 @@ #include "xremote_custom_view.h" #include "../xremote_app.h" +static void xremote_custom_view_draw_vertical(Canvas* canvas, XRemoteViewModel* model) { + XRemoteAppButtons* buttons = model->context; + + FuriString* text = model->hold && model->ok_pressed ? buttons->custom_ok_hold : + buttons->custom_ok; + xremote_canvas_draw_button_wide( + canvas, model->ok_pressed, 0, 27, furi_string_get_cstr(text), XRemoteIconEnter); + + text = model->hold && model->up_pressed ? buttons->custom_up_hold : buttons->custom_up; + xremote_canvas_draw_button_wide( + canvas, model->up_pressed, 0, 45, furi_string_get_cstr(text), XRemoteIconArrowUp); + + text = model->hold && model->down_pressed ? buttons->custom_down_hold : buttons->custom_down; + xremote_canvas_draw_button_wide( + canvas, model->down_pressed, 0, 63, furi_string_get_cstr(text), XRemoteIconArrowDown); + + text = model->hold && model->left_pressed ? buttons->custom_left_hold : buttons->custom_left; + xremote_canvas_draw_button_wide( + canvas, model->left_pressed, 0, 81, furi_string_get_cstr(text), XRemoteIconArrowLeft); + + text = model->hold && model->right_pressed ? buttons->custom_right_hold : + buttons->custom_right; + xremote_canvas_draw_button_wide( + canvas, model->right_pressed, 0, 99, furi_string_get_cstr(text), XRemoteIconArrowRight); +} + +static void xremote_custom_view_draw_horizontal(Canvas* canvas, XRemoteViewModel* model) { + XRemoteAppButtons* buttons = model->context; + + FuriString* text = model->hold && model->ok_pressed ? buttons->custom_ok_hold : + buttons->custom_ok; + xremote_canvas_draw_button_wide( + canvas, model->ok_pressed, 0, 7, furi_string_get_cstr(text), XRemoteIconEnter); + + text = model->hold && model->up_pressed ? buttons->custom_up_hold : buttons->custom_up; + xremote_canvas_draw_button_wide( + canvas, model->up_pressed, 0, 25, furi_string_get_cstr(text), XRemoteIconArrowUp); + + text = model->hold && model->down_pressed ? buttons->custom_down_hold : buttons->custom_down; + xremote_canvas_draw_button_wide( + canvas, model->down_pressed, 0, 43, furi_string_get_cstr(text), XRemoteIconArrowDown); + + text = model->hold && model->left_pressed ? buttons->custom_left_hold : buttons->custom_left; + xremote_canvas_draw_button_wide( + canvas, model->left_pressed, 64, 20, furi_string_get_cstr(text), XRemoteIconArrowLeft); + + text = model->hold && model->right_pressed ? buttons->custom_right_hold : + buttons->custom_right; + xremote_canvas_draw_button_wide( + canvas, model->right_pressed, 64, 38, furi_string_get_cstr(text), XRemoteIconArrowRight); +} + +static void xremote_custom_view_draw_page_name(Canvas* canvas, ViewOrientation orientation) { + Align align = orientation == ViewOrientationHorizontal ? AlignRight : AlignLeft; + uint8_t x = orientation == ViewOrientationHorizontal ? 128 : 0; + uint8_t y = orientation == ViewOrientationHorizontal ? 10 : 12; + elements_multiline_text_aligned(canvas, x, y, align, AlignTop, "Custom"); +} + static void xremote_custom_view_draw_callback(Canvas* canvas, void* context) { furi_assert(context); XRemoteViewModel* model = context; - XRemoteAppContext* app_ctx = model->context; + XRemoteAppButtons* buttons = model->context; + XRemoteAppContext* app_ctx = buttons->app_ctx; + const char* exit_str = xremote_app_context_get_exit_str(app_ctx); ViewOrientation orientation = app_ctx->app_settings->orientation; - uint64_t x = orientation == ViewOrientationVertical ? 70 : 34; - xremote_canvas_draw_header(canvas, orientation, "Custom"); - canvas_set_font(canvas, FontSecondary); - canvas_draw_str(canvas, 0, x, "Coming Soon."); - xremote_canvas_draw_exit_footer(canvas, orientation, "Press to exit"); + XRemoteViewDrawFunction xremote_custom_view_draw_body; + xremote_custom_view_draw_body = orientation == ViewOrientationVertical ? + xremote_custom_view_draw_vertical : + xremote_custom_view_draw_horizontal; + + xremote_canvas_draw_header(canvas, orientation, NULL); + xremote_custom_view_draw_page_name(canvas, orientation); + xremote_custom_view_draw_body(canvas, model); + xremote_canvas_draw_exit_footer(canvas, orientation, exit_str); } -XRemoteView* xremote_custom_view_alloc(void* app_ctx) { - XRemoteView* view = xremote_view_alloc(app_ctx, NULL, xremote_custom_view_draw_callback); +static void xremote_custom_view_process(XRemoteView* view, InputEvent* event) { + with_view_model( + xremote_view_get_view(view), + XRemoteViewModel * model, + { + XRemoteAppButtons* buttons = xremote_view_get_context(view); + XRemoteAppContext* app_ctx = buttons->app_ctx; + XRemoteAppExit exit_behavior = app_ctx->app_settings->exit_behavior; + model->context = buttons; + + if(event->type == InputTypeShort) { + InfraredRemoteButton* button = NULL; + + if(event->key == InputKeyOk) { + const char* button_name = furi_string_get_cstr(buttons->custom_ok); + button = xremote_view_get_button_by_name(view, button_name); + if(xremote_view_press_button(view, button)) model->ok_pressed = true; + } else if(event->key == InputKeyUp) { + const char* button_name = furi_string_get_cstr(buttons->custom_up); + button = xremote_view_get_button_by_name(view, button_name); + if(xremote_view_press_button(view, button)) model->up_pressed = true; + } else if(event->key == InputKeyDown) { + const char* button_name = furi_string_get_cstr(buttons->custom_down); + button = xremote_view_get_button_by_name(view, button_name); + if(xremote_view_press_button(view, button)) model->down_pressed = true; + } else if(event->key == InputKeyLeft) { + const char* button_name = furi_string_get_cstr(buttons->custom_left); + button = xremote_view_get_button_by_name(view, button_name); + if(xremote_view_press_button(view, button)) model->left_pressed = true; + } else if(event->key == InputKeyRight) { + const char* button_name = furi_string_get_cstr(buttons->custom_right); + button = xremote_view_get_button_by_name(view, button_name); + if(xremote_view_press_button(view, button)) model->right_pressed = true; + } else if(event->key == InputKeyBack && exit_behavior == XRemoteAppExitHold) { + button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_BACK); + if(xremote_view_press_button(view, button)) model->back_pressed = true; + } + } else if(event->type == InputTypeLong) { + InfraredRemoteButton* button = NULL; + model->hold = true; + + if(event->key == InputKeyOk) { + const char* button_name = furi_string_get_cstr(buttons->custom_ok_hold); + button = xremote_view_get_button_by_name(view, button_name); + if(xremote_view_press_button(view, button)) model->ok_pressed = true; + } else if(event->key == InputKeyUp) { + const char* button_name = furi_string_get_cstr(buttons->custom_up_hold); + button = xremote_view_get_button_by_name(view, button_name); + if(xremote_view_press_button(view, button)) model->up_pressed = true; + } else if(event->key == InputKeyDown) { + const char* button_name = furi_string_get_cstr(buttons->custom_down_hold); + button = xremote_view_get_button_by_name(view, button_name); + if(xremote_view_press_button(view, button)) model->down_pressed = true; + } else if(event->key == InputKeyLeft) { + const char* button_name = furi_string_get_cstr(buttons->custom_left_hold); + button = xremote_view_get_button_by_name(view, button_name); + if(xremote_view_press_button(view, button)) model->left_pressed = true; + } else if(event->key == InputKeyRight) { + const char* button_name = furi_string_get_cstr(buttons->custom_right_hold); + button = xremote_view_get_button_by_name(view, button_name); + if(xremote_view_press_button(view, button)) model->right_pressed = true; + } else if(event->key == InputKeyBack && exit_behavior == XRemoteAppExitPress) { + button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_BACK); + if(xremote_view_press_button(view, button)) model->back_pressed = true; + } + } else if(event->type == InputTypeRelease) { + model->hold = false; + + if(event->key == InputKeyOk) + model->ok_pressed = false; + else if(event->key == InputKeyUp) + model->up_pressed = false; + else if(event->key == InputKeyDown) + model->down_pressed = false; + else if(event->key == InputKeyLeft) + model->left_pressed = false; + else if(event->key == InputKeyRight) + model->right_pressed = false; + else if(event->key == InputKeyBack) + model->back_pressed = false; + } + }, + true); +} + +static bool xremote_custom_view_input_callback(InputEvent* event, void* context) { + furi_assert(context); + XRemoteView* view = (XRemoteView*)context; + XRemoteAppContext* app_ctx = xremote_view_get_app_context(view); + XRemoteAppExit exit_behavior = app_ctx->app_settings->exit_behavior; + + if(event->key == InputKeyBack) { + if(event->type == InputTypeShort && exit_behavior == XRemoteAppExitPress) + return false; + else if(event->type == InputTypeLong && exit_behavior == XRemoteAppExitHold) + return false; + } + + xremote_custom_view_process(view, event); + return true; +} + +XRemoteView* xremote_custom_view_alloc(void* app_ctx, void* model_ctx) { + XRemoteView* view = xremote_view_alloc( + app_ctx, xremote_custom_view_input_callback, xremote_custom_view_draw_callback); + xremote_view_set_context(view, model_ctx, NULL); with_view_model( - xremote_view_get_view(view), XRemoteViewModel * model, { model->context = app_ctx; }, true); + xremote_view_get_view(view), + XRemoteViewModel * model, + { + model->context = model_ctx; + model->up_pressed = false; + model->down_pressed = false; + model->left_pressed = false; + model->right_pressed = false; + model->back_pressed = false; + model->ok_pressed = false; + model->hold = false; + }, + true); return view; -} \ No newline at end of file +} diff --git a/views/xremote_custom_view.h b/views/xremote_custom_view.h index a864ee5..ed480f7 100644 --- a/views/xremote_custom_view.h +++ b/views/xremote_custom_view.h @@ -10,4 +10,4 @@ #include "xremote_common_view.h" -XRemoteView* xremote_custom_view_alloc(void* app_ctx); +XRemoteView* xremote_custom_view_alloc(void* app_ctx, void* model_ctx); diff --git a/views/xremote_general_view.c b/views/xremote_general_view.c index 4347cb1..48b557e 100644 --- a/views/xremote_general_view.c +++ b/views/xremote_general_view.c @@ -26,25 +26,35 @@ static void xremote_general_view_draw_horizontal(Canvas* canvas, XRemoteViewMode xremote_canvas_draw_button_wide( canvas, model->down_pressed, 0, 43, "Setup", XRemoteIconArrowDown); xremote_canvas_draw_button_wide( - canvas, model->left_pressed, 64, 21, "Menu", XRemoteIconArrowLeft); + canvas, model->left_pressed, 64, 20, "Menu", XRemoteIconArrowLeft); xremote_canvas_draw_button_wide( - canvas, model->right_pressed, 64, 39, "List", XRemoteIconArrowRight); + canvas, model->right_pressed, 64, 38, "List", XRemoteIconArrowRight); +} + +static void xremote_general_view_draw_page_name(Canvas* canvas, ViewOrientation orientation) { + Align align = orientation == ViewOrientationHorizontal ? AlignRight : AlignLeft; + uint8_t x = orientation == ViewOrientationHorizontal ? 128 : 0; + uint8_t y = orientation == ViewOrientationHorizontal ? 10 : 12; + elements_multiline_text_aligned(canvas, x, y, align, AlignTop, "General"); } static void xremote_general_view_draw_callback(Canvas* canvas, void* context) { furi_assert(context); XRemoteViewModel* model = context; XRemoteAppContext* app_ctx = model->context; - XRemoteViewDrawFunction xremote_general_view_draw_body; + const char* exit_str = xremote_app_context_get_exit_str(app_ctx); ViewOrientation orientation = app_ctx->app_settings->orientation; + + XRemoteViewDrawFunction xremote_general_view_draw_body; xremote_general_view_draw_body = orientation == ViewOrientationVertical ? xremote_general_view_draw_vertical : xremote_general_view_draw_horizontal; - xremote_canvas_draw_header(canvas, orientation, "General"); + xremote_canvas_draw_header(canvas, orientation, NULL); + xremote_general_view_draw_page_name(canvas, orientation); xremote_general_view_draw_body(canvas, model); - xremote_canvas_draw_exit_footer(canvas, orientation, "Press to exit"); + xremote_canvas_draw_exit_footer(canvas, orientation, exit_str); } static void xremote_general_view_process(XRemoteView* view, InputEvent* event) { @@ -52,10 +62,13 @@ static void xremote_general_view_process(XRemoteView* view, InputEvent* event) { xremote_view_get_view(view), XRemoteViewModel * model, { - if(event->type == InputTypePress) { - model->context = xremote_view_get_app_context(view); - InfraredRemoteButton* button = NULL; + XRemoteAppContext* app_ctx = xremote_view_get_app_context(view); + XRemoteAppExit exit_behavior = app_ctx->app_settings->exit_behavior; + InfraredRemoteButton* button = NULL; + model->context = app_ctx; + + if(event->type == InputTypePress) { if(event->key == InputKeyOk) { button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_POWER); if(xremote_view_press_button(view, button)) model->ok_pressed = true; @@ -72,6 +85,12 @@ static void xremote_general_view_process(XRemoteView* view, InputEvent* event) { button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_LIST); if(xremote_view_press_button(view, button)) model->right_pressed = true; } + } else if(event->key == InputKeyBack) { + if((event->type == InputTypeShort && exit_behavior == XRemoteAppExitHold) || + (event->type == InputTypeLong && exit_behavior == XRemoteAppExitPress)) { + button = xremote_view_get_button_by_name(view, XREMOTE_COMMAND_BACK); + if(xremote_view_press_button(view, button)) model->back_pressed = true; + } } else if(event->type == InputTypeRelease) { if(event->key == InputKeyOk) model->ok_pressed = false; @@ -83,6 +102,8 @@ static void xremote_general_view_process(XRemoteView* view, InputEvent* event) { model->left_pressed = false; else if(event->key == InputKeyRight) model->right_pressed = false; + else if(event->key == InputKeyBack) + model->back_pressed = false; } }, true); @@ -91,7 +112,15 @@ static void xremote_general_view_process(XRemoteView* view, InputEvent* event) { static bool xremote_general_view_input_callback(InputEvent* event, void* context) { furi_assert(context); XRemoteView* view = (XRemoteView*)context; - if(event->key == InputKeyBack) return false; + XRemoteAppContext* app_ctx = xremote_view_get_app_context(view); + XRemoteAppExit exit_behavior = app_ctx->app_settings->exit_behavior; + + if(event->key == InputKeyBack) { + if(event->type == InputTypeShort && exit_behavior == XRemoteAppExitPress) + return false; + else if(event->type == InputTypeLong && exit_behavior == XRemoteAppExitHold) + return false; + } xremote_general_view_process(view, event); return true; diff --git a/xremote.c b/xremote.c index 43a91d8..83cd88c 100644 --- a/xremote.c +++ b/xremote.c @@ -19,13 +19,7 @@ #define TAG "XRemote" void xremote_get_version(char* version, size_t length) { - snprintf( - version, - length, - "%d.%d.%d", - XREMOTE_VERSION_MAJOR, - XREMOTE_VERSION_MINOR, - XREMOTE_BUILD_NUMBER); + snprintf(version, length, "%.1f.%d", (double)FAP_VERSION, XREMOTE_BUILD_NUMBER); } static uint32_t xremote_view_exit_callback(void* context) { diff --git a/xremote.h b/xremote.h index ec63288..0fe4e64 100644 --- a/xremote.h +++ b/xremote.h @@ -8,8 +8,7 @@ #include "xremote_app.h" -#define XREMOTE_VERSION_MAJOR 1 -#define XREMOTE_VERSION_MINOR 0 -#define XREMOTE_BUILD_NUMBER 5 +#define XREMOTE_BUILD_NUMBER 2 +/* Returns FAP_VERSION + XREMOTE_BUILD_NUMBER */ void xremote_get_version(char* version, size_t length); diff --git a/xremote_app.c b/xremote_app.c index c640136..2f0908e 100644 --- a/xremote_app.c +++ b/xremote_app.c @@ -8,9 +8,23 @@ #include "xremote_app.h" +////////////////////////////////////////////////////////////////////////////// +// XRemote generic functions and definitions +////////////////////////////////////////////////////////////////////////////// + #define XREMOTE_APP_SETTINGS APP_DATA_PATH("xremote.cfg") #define TAG "XRemoteApp" +#define XREMOTE_ORIENTATION_TEXT_HORIZONTAL "Horizontal" +#define XREMOTE_ORIENTATION_TEXT_VERTICAL "Vertical" +#define XREMOTE_ORIENTATION_INDEX_HORIZONTAL 0 +#define XREMOTE_ORIENTATION_INDEX_VERTICAL 1 + +#define XREMOTE_EXIT_BEHAVIOR_TEXT_PRESS "Press" +#define XREMOTE_EXIT_BEHAVIOR_TEXT_HOLD "Hold" +#define XREMOTE_EXIT_BEHAVIOR_INDEX_PRESS 0 +#define XREMOTE_EXIT_BEHAVIOR_INDEX_HOLD 1 + const NotificationSequence g_sequence_blink_purple_50 = { &message_red_255, &message_blue_255, @@ -18,6 +32,176 @@ const NotificationSequence g_sequence_blink_purple_50 = { NULL, }; +XRemoteAppExit xremote_app_get_exit_behavior(uint8_t exit_index) { + return exit_index ? XRemoteAppExitHold : XRemoteAppExitPress; +} + +ViewOrientation xremote_app_get_orientation(uint8_t orientation_index) { + return orientation_index ? ViewOrientationVertical : ViewOrientationHorizontal; +} + +const char* xremote_app_get_exit_str(XRemoteAppExit exit_behavior) { + return exit_behavior == XRemoteAppExitPress ? "Press" : "Hold"; +} + +const char* xremote_app_get_orientation_str(ViewOrientation view_orientation) { + return view_orientation == ViewOrientationHorizontal ? "Horizontal" : "Vertical"; +} + +uint32_t xremote_app_get_orientation_index(ViewOrientation view_orientation) { + return view_orientation == ViewOrientationHorizontal ? 0 : 1; +} + +uint32_t xremote_app_get_exit_index(XRemoteAppExit exit_behavior) { + return exit_behavior == XRemoteAppExitPress ? 0 : 1; +} + +void xremote_app_notification_blink(NotificationApp* notifications) { + xremote_app_assert_void(notifications); + notification_message(notifications, &g_sequence_blink_purple_50); +} + +////////////////////////////////////////////////////////////////////////////// +// XRemote buttons and custom button pairs +////////////////////////////////////////////////////////////////////////////// + +bool xremote_app_extension_load(XRemoteAppButtons* buttons, FuriString* path) { + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* ff = flipper_format_buffered_file_alloc(storage); + FuriString* tmp = furi_string_alloc(); + bool success = false; + + do { + /* Open file and read the header */ + if(!flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(path))) break; + + if(!flipper_format_read_string(ff, "custom_ok", tmp)) break; + furi_string_set(buttons->custom_ok, tmp); + + if(!flipper_format_read_string(ff, "custom_up", tmp)) break; + furi_string_set(buttons->custom_up, tmp); + + if(!flipper_format_read_string(ff, "custom_down", tmp)) break; + furi_string_set(buttons->custom_down, tmp); + + if(!flipper_format_read_string(ff, "custom_left", tmp)) break; + furi_string_set(buttons->custom_left, tmp); + + if(!flipper_format_read_string(ff, "custom_right", tmp)) break; + furi_string_set(buttons->custom_right, tmp); + + if(!flipper_format_read_string(ff, "custom_ok_hold", tmp)) break; + furi_string_set(buttons->custom_ok_hold, tmp); + + if(!flipper_format_read_string(ff, "custom_up_hold", tmp)) break; + furi_string_set(buttons->custom_up_hold, tmp); + + if(!flipper_format_read_string(ff, "custom_down_hold", tmp)) break; + furi_string_set(buttons->custom_down_hold, tmp); + + if(!flipper_format_read_string(ff, "custom_left_hold", tmp)) break; + furi_string_set(buttons->custom_left_hold, tmp); + + if(!flipper_format_read_string(ff, "custom_right_hold", tmp)) break; + furi_string_set(buttons->custom_right_hold, tmp); + + success = true; + } while(false); + + furi_record_close(RECORD_STORAGE); + flipper_format_free(ff); + furi_string_free(tmp); + + return success; +} + +bool xremote_app_extension_store(XRemoteAppButtons* buttons, FuriString* path) { + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* ff = flipper_format_file_alloc(storage); + bool success = false; + + do { + if(!flipper_format_file_open_append(ff, furi_string_get_cstr(path))) break; + if(!flipper_format_write_comment_cstr(ff, "XRemote extension")) break; + + if(!flipper_format_write_string(ff, "custom_ok", buttons->custom_ok)) break; + if(!flipper_format_write_string(ff, "custom_up", buttons->custom_up)) break; + if(!flipper_format_write_string(ff, "custom_down", buttons->custom_down)) break; + if(!flipper_format_write_string(ff, "custom_left", buttons->custom_left)) break; + if(!flipper_format_write_string(ff, "custom_right", buttons->custom_right)) break; + if(!flipper_format_write_string(ff, "custom_ok_hold", buttons->custom_ok_hold)) break; + if(!flipper_format_write_string(ff, "custom_up_hold", buttons->custom_up_hold)) break; + if(!flipper_format_write_string(ff, "custom_down_hold", buttons->custom_down_hold)) break; + if(!flipper_format_write_string(ff, "custom_left_hold", buttons->custom_left_hold)) break; + if(!flipper_format_write_string(ff, "custom_right_hold", buttons->custom_right_hold)) + break; + + success = true; + } while(false); + + furi_record_close(RECORD_STORAGE); + flipper_format_free(ff); + + return success; +} + +void xremote_app_buttons_free(XRemoteAppButtons* buttons) { + xremote_app_assert_void(buttons); + infrared_remote_free(buttons->remote); + furi_string_free(buttons->custom_up); + furi_string_free(buttons->custom_down); + furi_string_free(buttons->custom_left); + furi_string_free(buttons->custom_right); + furi_string_free(buttons->custom_ok); + furi_string_free(buttons->custom_up_hold); + furi_string_free(buttons->custom_down_hold); + furi_string_free(buttons->custom_left_hold); + furi_string_free(buttons->custom_right_hold); + furi_string_free(buttons->custom_ok_hold); + free(buttons); +} + +XRemoteAppButtons* xremote_app_buttons_alloc() { + XRemoteAppButtons* buttons = malloc(sizeof(XRemoteAppButtons)); + buttons->remote = infrared_remote_alloc(); + buttons->app_ctx = NULL; + + /* Setup default buttons for custom layout */ + buttons->custom_up = furi_string_alloc_set_str(XREMOTE_COMMAND_UP); + buttons->custom_down = furi_string_alloc_set_str(XREMOTE_COMMAND_DOWN); + buttons->custom_left = furi_string_alloc_set_str(XREMOTE_COMMAND_LEFT); + buttons->custom_right = furi_string_alloc_set_str(XREMOTE_COMMAND_RIGHT); + buttons->custom_ok = furi_string_alloc_set_str(XREMOTE_COMMAND_OK); + buttons->custom_up_hold = furi_string_alloc_set_str(XREMOTE_COMMAND_INPUT); + buttons->custom_down_hold = furi_string_alloc_set_str(XREMOTE_COMMAND_SETUP); + buttons->custom_left_hold = furi_string_alloc_set_str(XREMOTE_COMMAND_MENU); + buttons->custom_right_hold = furi_string_alloc_set_str(XREMOTE_COMMAND_LIST); + buttons->custom_ok_hold = furi_string_alloc_set_str(XREMOTE_COMMAND_POWER); + + return buttons; +} + +XRemoteAppButtons* xremote_app_buttons_load(XRemoteAppContext* app_ctx) { + /* Show file selection dialog (returns selected file path with app_ctx->file_path) */ + if(!xremote_app_browser_select_file(app_ctx, XREMOTE_APP_EXTENSION)) return NULL; + XRemoteAppButtons* buttons = xremote_app_buttons_alloc(); + buttons->app_ctx = app_ctx; + + /* Load buttons from the selected path */ + if(!infrared_remote_load(buttons->remote, app_ctx->file_path)) { + xremote_app_buttons_free(buttons); + return NULL; + } + + /* Load custom buttons from the selected path */ + xremote_app_extension_load(buttons, app_ctx->file_path); + return buttons; +} + +////////////////////////////////////////////////////////////////////////////// +// XRemote application settings +////////////////////////////////////////////////////////////////////////////// + XRemoteAppSettings* xremote_app_settings_alloc() { XRemoteAppSettings* settings = malloc(sizeof(XRemoteAppSettings)); settings->orientation = ViewOrientationHorizontal; @@ -45,10 +229,10 @@ bool xremote_app_settings_store(XRemoteAppSettings* settings) { if(!flipper_format_write_comment_cstr(ff, "")) break; /* Write actual configuration to the settings file */ - uint32_t value = settings->orientation == ViewOrientationHorizontal ? 0 : 1; + uint32_t value = xremote_app_get_orientation_index(settings->orientation); if(!flipper_format_write_uint32(ff, "orientation", &value, 1)) break; - value = settings->exit_behavior == XRemoteAppExitPress ? 0 : 1; + value = xremote_app_get_exit_index(settings->exit_behavior); if(!flipper_format_write_uint32(ff, "appexit", &value, 1)) break; value = settings->repeat_count; @@ -81,9 +265,11 @@ bool xremote_app_settings_load(XRemoteAppSettings* settings) { /* Parse config data from the buffer */ if(!flipper_format_read_uint32(ff, "orientation", &value, 1)) break; - settings->orientation = value == 0 ? ViewOrientationHorizontal : ViewOrientationVertical; + settings->orientation = xremote_app_get_orientation(value); + if(!flipper_format_read_uint32(ff, "appexit", &value, 1)) break; - settings->exit_behavior = value == 0 ? XRemoteAppExitPress : XRemoteAppExitHold; + settings->exit_behavior = xremote_app_get_exit_behavior(value); + if(!flipper_format_read_uint32(ff, "repeat", &value, 1)) break; settings->repeat_count = value; @@ -97,6 +283,10 @@ bool xremote_app_settings_load(XRemoteAppSettings* settings) { return success; } +////////////////////////////////////////////////////////////////////////////// +// XRemote gloal context shared between every child application +////////////////////////////////////////////////////////////////////////////// + XRemoteAppContext* xremote_app_context_alloc(void* arg) { XRemoteAppContext* ctx = malloc(sizeof(XRemoteAppContext)); ctx->app_argument = arg; @@ -136,14 +326,35 @@ void xremote_app_context_free(XRemoteAppContext* ctx) { free(ctx); } -const char* xremote_app_context_get_exit_str(XRemoteAppContext* ctx) { - XRemoteAppExit exit_behavior = ctx->app_settings->exit_behavior; - return exit_behavior == XRemoteAppExitHold ? "Hold to exit" : "Press to exit"; +bool xremote_app_browser_select_file(XRemoteAppContext* app_ctx, const char* extension) { + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + Storage* storage = furi_record_open(RECORD_STORAGE); + storage_simply_mkdir(storage, XREMOTE_APP_FOLDER); + + if(app_ctx->file_path == NULL) { + app_ctx->file_path = furi_string_alloc(); + furi_string_set(app_ctx->file_path, XREMOTE_APP_FOLDER); + } + + /* Open file browser (view and dialogs are managed by the browser itself) */ + DialogsFileBrowserOptions browser; + dialog_file_browser_set_basic_options(&browser, extension, &I_IR_Icon_10x10); + browser.base_path = XREMOTE_APP_FOLDER; + FuriString* path = app_ctx->file_path; + + /* Show file selection dialog (returns selected file path with file_path) */ + bool status = dialog_file_browser_show(dialogs, path, path, &browser); + + /* Cleanup file loading context */ + furi_record_close(RECORD_STORAGE); + furi_record_close(RECORD_DIALOGS); + + return status; } -void xremote_app_notification_blink(NotificationApp* notifications) { - xremote_app_assert_void(notifications); - notification_message(notifications, &g_sequence_blink_purple_50); +const char* xremote_app_context_get_exit_str(XRemoteAppContext* app_ctx) { + XRemoteAppExit exit_behavior = app_ctx->app_settings->exit_behavior; + return exit_behavior == XRemoteAppExitHold ? "Hold to exit" : "Press to exit"; } void xremote_app_context_notify_led(XRemoteAppContext* app_ctx) { @@ -159,6 +370,10 @@ bool xremote_app_send_signal(XRemoteAppContext* app_ctx, InfraredSignal* signal) return true; } +////////////////////////////////////////////////////////////////////////////// +// XRemote application factory +////////////////////////////////////////////////////////////////////////////// + void xremote_app_view_alloc(XRemoteApp* app, uint32_t view_id, XRemoteViewAllocator allocator) { furi_assert(app); xremote_app_assert_void(app->app_ctx); @@ -175,6 +390,26 @@ void xremote_app_view_alloc(XRemoteApp* app, uint32_t view_id, XRemoteViewAlloca view_dispatcher_add_view(view_disp, app->view_id, app_view); } +void xremote_app_view_alloc2( + XRemoteApp* app, + uint32_t view_id, + XRemoteViewAllocator2 allocator, + void* model_ctx) { + furi_assert(app); + xremote_app_assert_void(app->app_ctx); + + if(app->view_id == view_id && app->view_ctx != NULL) return; + + xremote_app_view_free(app); + app->view_id = view_id; + + app->view_ctx = allocator(app->app_ctx, model_ctx); + View* app_view = xremote_view_get_view(app->view_ctx); + + ViewDispatcher* view_disp = app->app_ctx->view_dispatcher; + view_dispatcher_add_view(view_disp, app->view_id, app_view); +} + void xremote_app_view_free(XRemoteApp* app) { xremote_app_assert_void(app); @@ -286,7 +521,10 @@ void xremote_app_user_context_free(XRemoteApp* app) { XRemoteApp* xremote_app_alloc(XRemoteAppContext* ctx) { furi_assert(ctx); + XRemoteApp* app = malloc(sizeof(XRemoteApp)); + xremote_app_assert(app, NULL); + app->submenu_id = XRemoteViewNone; app->view_id = XRemoteViewNone; app->app_ctx = ctx; @@ -294,6 +532,7 @@ XRemoteApp* xremote_app_alloc(XRemoteAppContext* ctx) { app->view_ctx = NULL; app->on_clear = NULL; app->context = NULL; + return app; } diff --git a/xremote_app.h b/xremote_app.h index 4c07056..1f0a3ac 100644 --- a/xremote_app.h +++ b/xremote_app.h @@ -28,6 +28,10 @@ #include "views/xremote_common_view.h" #include "xc_icons.h" +////////////////////////////////////////////////////////////////////////////// +// XRemote generic functions and definitions +////////////////////////////////////////////////////////////////////////////// + #define XREMOTE_APP_EXTENSION ".ir" #define XREMOTE_APP_FOLDER ANY_PATH("infrared") #define XREMOTE_APP_TEXT_MAX 128 @@ -39,6 +43,18 @@ typedef enum { XRemoteAppExitPress, XRemoteAppExitHold } XRemoteAppExit; +XRemoteAppExit xremote_app_get_exit_behavior(uint8_t exit_index); +const char* xremote_app_get_exit_str(XRemoteAppExit exit_behavior); +uint32_t xremote_app_get_exit_index(XRemoteAppExit exit_behavior); + +ViewOrientation xremote_app_get_orientation(uint8_t orientation_index); +const char* xremote_app_get_orientation_str(ViewOrientation view_orientation); +uint32_t xremote_app_get_orientation_index(ViewOrientation view_orientation); + +////////////////////////////////////////////////////////////////////////////// +// XRemote application settings +////////////////////////////////////////////////////////////////////////////// + typedef struct { ViewOrientation orientation; XRemoteAppExit exit_behavior; @@ -51,6 +67,10 @@ void xremote_app_settings_free(XRemoteAppSettings* settings); bool xremote_app_settings_store(XRemoteAppSettings* settings); bool xremote_app_settings_load(XRemoteAppSettings* settings); +////////////////////////////////////////////////////////////////////////////// +// XRemote gloal context shared between every child application +////////////////////////////////////////////////////////////////////////////// + typedef struct { XRemoteAppSettings* app_settings; NotificationApp* notifications; @@ -63,10 +83,41 @@ typedef struct { XRemoteAppContext* xremote_app_context_alloc(void* arg); void xremote_app_context_free(XRemoteAppContext* ctx); -const char* xremote_app_context_get_exit_str(XRemoteAppContext* ctx); +const char* xremote_app_context_get_exit_str(XRemoteAppContext* app_ctx); void xremote_app_context_notify_led(XRemoteAppContext* app_ctx); void xremote_app_notification_blink(NotificationApp* notifications); bool xremote_app_send_signal(XRemoteAppContext* app_ctx, InfraredSignal* signal); +bool xremote_app_browser_select_file(XRemoteAppContext* app_ctx, const char* extension); + +////////////////////////////////////////////////////////////////////////////// +// XRemote buttons and custom button pairs +////////////////////////////////////////////////////////////////////////////// + +typedef struct { + XRemoteAppContext* app_ctx; + InfraredRemote* remote; + FuriString* custom_up; + FuriString* custom_down; + FuriString* custom_left; + FuriString* custom_right; + FuriString* custom_ok; + FuriString* custom_up_hold; + FuriString* custom_down_hold; + FuriString* custom_left_hold; + FuriString* custom_right_hold; + FuriString* custom_ok_hold; +} XRemoteAppButtons; + +void xremote_app_buttons_free(XRemoteAppButtons* buttons); +XRemoteAppButtons* xremote_app_buttons_alloc(); +XRemoteAppButtons* xremote_app_buttons_load(XRemoteAppContext* app_ctx); + +bool xremote_app_extension_store(XRemoteAppButtons* buttons, FuriString* path); +bool xremote_app_extension_load(XRemoteAppButtons* buttons, FuriString* path); + +////////////////////////////////////////////////////////////////////////////// +// XRemote application factory +////////////////////////////////////////////////////////////////////////////// typedef struct { XRemoteClearCallback on_clear; @@ -87,6 +138,11 @@ void xremote_app_submenu_add( void xremote_app_submenu_alloc(XRemoteApp* app, uint32_t index, ViewNavigationCallback prev_cb); void xremote_app_submenu_free(XRemoteApp* app); +void xremote_app_view_alloc2( + XRemoteApp* app, + uint32_t view_id, + XRemoteViewAllocator2 allocator, + void* model_ctx); void xremote_app_view_alloc(XRemoteApp* app, uint32_t view_id, XRemoteViewAllocator allocator); void xremote_app_view_free(XRemoteApp* app); diff --git a/xremote_control.c b/xremote_control.c index 98a9ce9..1d20955 100644 --- a/xremote_control.c +++ b/xremote_control.c @@ -7,6 +7,7 @@ */ #include "xremote_control.h" +#include "xremote_edit.h" #include "infrared/infrared_remote.h" #include "views/xremote_general_view.h" @@ -20,14 +21,14 @@ static uint32_t xremote_control_submenu_exit_callback(void* context) { return XRemoteViewSubmenu; } -static uint32_t xremote_navigation_view_exit_callback(void* context) { +static uint32_t xremote_control_view_exit_callback(void* context) { UNUSED(context); return XRemoteViewIRSubmenu; } -static void xremote_ir_clear_callback(void* context) { +static void xremote_buttons_clear_callback(void* context) { xremote_app_assert_void(context); - infrared_remote_free((InfraredRemote*)context); + xremote_app_buttons_free((XRemoteAppButtons*)context); } static void xremote_control_submenu_callback(void* context, uint32_t index) { @@ -41,64 +42,33 @@ static void xremote_control_submenu_callback(void* context, uint32_t index) { xremote_app_view_alloc(app, index, xremote_control_view_alloc); else if(index == XRemoteViewIRNavigation) xremote_app_view_alloc(app, index, xremote_navigation_view_alloc); - else if(index == XRemoteViewIRPlayer) + else if(index == XRemoteViewIRPlayback) xremote_app_view_alloc(app, index, xremote_player_view_alloc); - else if(index == XRemoteViewIRCustom) - xremote_app_view_alloc(app, index, xremote_custom_view_alloc); + else if(index == XRemoteViewIRCustomPage) + xremote_app_view_alloc2(app, index, xremote_custom_view_alloc, app->context); + else if(index == XRemoteViewIRCustomEditPage) + xremote_edit_view_alloc(app, index, app->context); if(app->view_ctx != NULL) { - xremote_app_view_set_previous_callback(app, xremote_navigation_view_exit_callback); - xremote_app_set_view_context(app, app->context, NULL); - xremote_app_switch_to_view(app, index); - } -} - -static InfraredRemote* xremote_load_ir_buttons(XRemoteAppContext* app_ctx) { - DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); - Storage* storage = furi_record_open(RECORD_STORAGE); - storage_simply_mkdir(storage, XREMOTE_APP_FOLDER); - - /* Open file browser (view and dialogs are managed by the browser itself) */ - DialogsFileBrowserOptions browser; - dialog_file_browser_set_basic_options(&browser, XREMOTE_APP_EXTENSION, &I_IR_Icon_10x10); - browser.base_path = XREMOTE_APP_FOLDER; - - if(app_ctx->file_path == NULL) { - app_ctx->file_path = furi_string_alloc(); - furi_string_set(app_ctx->file_path, XREMOTE_APP_FOLDER); - } - - /* Show file selection dialog (returns selected file path with variable file_path) */ - if(!dialog_file_browser_show(dialogs, app_ctx->file_path, app_ctx->file_path, &browser)) { - furi_record_close(RECORD_STORAGE); - furi_record_close(RECORD_DIALOGS); - return NULL; - } - - /* Load buttons from the selected path */ - InfraredRemote* remote = infrared_remote_alloc(); - bool success = infrared_remote_load(remote, app_ctx->file_path); + if(index != XRemoteViewIRCustomEditPage) { + xremote_app_view_set_previous_callback(app, xremote_control_view_exit_callback); + xremote_app_set_view_context(app, app->context, NULL); + } - /* Cleanup file loading context */ - furi_record_close(RECORD_STORAGE); - furi_record_close(RECORD_DIALOGS); - - if(!success) { - infrared_remote_free(remote); - return NULL; + xremote_app_switch_to_view(app, index); } - - return remote; } XRemoteApp* xremote_control_alloc(XRemoteAppContext* app_ctx) { /* Open file browser and load buttons from selected file */ - InfraredRemote* remote = xremote_load_ir_buttons(app_ctx); - xremote_app_assert(remote, NULL); + XRemoteAppButtons* buttons = xremote_app_buttons_load(app_ctx); + xremote_app_assert(buttons, NULL); /* Allocate remote controller app with submenu */ XRemoteApp* app = xremote_app_alloc(app_ctx); + xremote_app_set_user_context(app, buttons, xremote_buttons_clear_callback); xremote_app_submenu_alloc(app, XRemoteViewIRSubmenu, xremote_control_submenu_exit_callback); + xremote_app_submenu_add( app, "General", XRemoteViewIRGeneral, xremote_control_submenu_callback); xremote_app_submenu_add( @@ -106,9 +76,11 @@ XRemoteApp* xremote_control_alloc(XRemoteAppContext* app_ctx) { xremote_app_submenu_add( app, "Navigation", XRemoteViewIRNavigation, xremote_control_submenu_callback); xremote_app_submenu_add( - app, "Playback", XRemoteViewIRPlayer, xremote_control_submenu_callback); - xremote_app_submenu_add(app, "Custom", XRemoteViewIRCustom, xremote_control_submenu_callback); - xremote_app_set_user_context(app, remote, xremote_ir_clear_callback); + app, "Playback", XRemoteViewIRPlayback, xremote_control_submenu_callback); + xremote_app_submenu_add( + app, "Custom", XRemoteViewIRCustomPage, xremote_control_submenu_callback); + xremote_app_submenu_add( + app, "Edit", XRemoteViewIRCustomEditPage, xremote_control_submenu_callback); return app; } diff --git a/xremote_edit.c b/xremote_edit.c new file mode 100644 index 0000000..9f53304 --- /dev/null +++ b/xremote_edit.c @@ -0,0 +1,162 @@ +/*! + * @file flipper-xremote/xremote_edit.c + @license This project is released under the GNU GPLv3 License + * @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com) + * + * @brief Edit menu for XRemote custom layout buttons. + */ + +#include "xremote_edit.h" + +typedef struct { + VariableItemList* item_list; + XRemoteAppButtons* buttons; +} XRemoteEditContext; + +static uint32_t xremote_edit_view_exit_callback(void* context) { + UNUSED(context); + return XRemoteViewIRSubmenu; +} + +static void xremote_edit_buttons_store(XRemoteAppButtons* buttons) { + FuriString* path = buttons->app_ctx->file_path; + infrared_remote_store(buttons->remote); + xremote_app_extension_store(buttons, path); +} + +static void xremote_item_update_item(VariableItem* item, FuriString* button) { + XRemoteEditContext* ctx = variable_item_get_context(item); + XRemoteAppButtons* buttons = ctx->buttons; + + int button_index = variable_item_get_current_value_index(item); + const char* button_name = xremote_button_get_name(button_index); + variable_item_set_current_value_text(item, button_name); + + furi_string_set_str(button, button_name); + xremote_edit_buttons_store(buttons); +} + +static void xremote_edit_ok_press_changed(VariableItem* item) { + XRemoteEditContext* ctx = variable_item_get_context(item); + xremote_item_update_item(item, ctx->buttons->custom_ok); +} + +static void xremote_edit_up_press_changed(VariableItem* item) { + XRemoteEditContext* ctx = variable_item_get_context(item); + xremote_item_update_item(item, ctx->buttons->custom_up); +} + +static void xremote_edit_down_press_changed(VariableItem* item) { + XRemoteEditContext* ctx = variable_item_get_context(item); + xremote_item_update_item(item, ctx->buttons->custom_down); +} + +static void xremote_edit_left_press_changed(VariableItem* item) { + XRemoteEditContext* ctx = variable_item_get_context(item); + xremote_item_update_item(item, ctx->buttons->custom_left); +} + +static void xremote_edit_right_press_changed(VariableItem* item) { + XRemoteEditContext* ctx = variable_item_get_context(item); + xremote_item_update_item(item, ctx->buttons->custom_right); +} + +static void xremote_edit_ok_hold_changed(VariableItem* item) { + XRemoteEditContext* ctx = variable_item_get_context(item); + xremote_item_update_item(item, ctx->buttons->custom_ok_hold); +} + +static void xremote_edit_up_hold_changed(VariableItem* item) { + XRemoteEditContext* ctx = variable_item_get_context(item); + xremote_item_update_item(item, ctx->buttons->custom_up_hold); +} + +static void xremote_edit_down_hold_changed(VariableItem* item) { + XRemoteEditContext* ctx = variable_item_get_context(item); + xremote_item_update_item(item, ctx->buttons->custom_down_hold); +} + +static void xremote_edit_left_hold_changed(VariableItem* item) { + XRemoteEditContext* ctx = variable_item_get_context(item); + xremote_item_update_item(item, ctx->buttons->custom_left_hold); +} + +static void xremote_edit_right_hold_changed(VariableItem* item) { + XRemoteEditContext* ctx = variable_item_get_context(item); + xremote_item_update_item(item, ctx->buttons->custom_right_hold); +} + +static void xremote_edit_list_add_item( + XRemoteEditContext* context, + const char* item_name, + FuriString* button, + VariableItemChangeCallback change_callback) { + VariableItemList* list = context->item_list; + VariableItem* item; + + /* Add custom_up to variable item list */ + item = variable_item_list_add(list, item_name, XREMOTE_BUTTON_COUNT, change_callback, context); + + /* Get button name and index */ + const char* button_name = furi_string_get_cstr(button); + uint32_t button_index = xremote_button_get_index(button_name); + + /* Set button name and index to the list item */ + variable_item_set_current_value_index(item, button_index); + variable_item_set_current_value_text(item, button_name); +} + +static XRemoteEditContext* xremote_edit_context_alloc(XRemoteAppButtons* buttons) { + XRemoteEditContext* context = malloc(sizeof(XRemoteEditContext)); + context->item_list = variable_item_list_alloc(); + + XRemoteAppContext* app_ctx = buttons->app_ctx; + context->buttons = buttons; + + /* Configure variable item list view */ + View* view = variable_item_list_get_view(context->item_list); + view_set_previous_callback(view, xremote_edit_view_exit_callback); + view_dispatcher_add_view(app_ctx->view_dispatcher, XRemoteViewIRCustomEditPage, view); + + /* Add press items to the variable list */ + xremote_edit_list_add_item( + context, "Ok press", buttons->custom_ok, xremote_edit_ok_press_changed); + xremote_edit_list_add_item( + context, "Up press", buttons->custom_up, xremote_edit_up_press_changed); + xremote_edit_list_add_item( + context, "Down press", buttons->custom_down, xremote_edit_down_press_changed); + xremote_edit_list_add_item( + context, "Left press", buttons->custom_left, xremote_edit_left_press_changed); + xremote_edit_list_add_item( + context, "Right press", buttons->custom_right, xremote_edit_right_press_changed); + xremote_edit_list_add_item( + context, "Ok hold", buttons->custom_ok_hold, xremote_edit_ok_hold_changed); + xremote_edit_list_add_item( + context, "Up hold", buttons->custom_up_hold, xremote_edit_up_hold_changed); + xremote_edit_list_add_item( + context, "Down hold", buttons->custom_down_hold, xremote_edit_down_hold_changed); + xremote_edit_list_add_item( + context, "Left hold", buttons->custom_left_hold, xremote_edit_left_hold_changed); + xremote_edit_list_add_item( + context, "Right hold", buttons->custom_right_hold, xremote_edit_right_hold_changed); + + return context; +} + +void xremote_edit_context_clear_callback(void* context) { + XRemoteEditContext* ctx = (XRemoteEditContext*)context; + variable_item_list_free(ctx->item_list); +} + +void xremote_edit_view_alloc(XRemoteApp* app, uint32_t view_id, XRemoteAppButtons* buttons) { + xremote_app_view_free(app); + XRemoteView* remote_view = xremote_view_alloc_empty(); + XRemoteEditContext* context = xremote_edit_context_alloc(buttons); + + xremote_view_set_app_context(remote_view, buttons->app_ctx); + xremote_view_set_view(remote_view, variable_item_list_get_view(context->item_list)); + xremote_view_set_context(remote_view, context, xremote_edit_context_clear_callback); + + app->view_ctx = remote_view; + app->view_id = view_id; +} diff --git a/xremote_edit.h b/xremote_edit.h new file mode 100644 index 0000000..ce5752b --- /dev/null +++ b/xremote_edit.h @@ -0,0 +1,13 @@ +/*! + * @file flipper-xremote/xremote_edit.h + @license This project is released under the GNU GPLv3 License + * @copyright (c) 2023 Sandro Kalatozishvili (s.kalatoz@gmail.com) + * + * @brief Edit menu for XRemote custom layout buttons. + */ + +#pragma once + +#include "xremote_app.h" + +void xremote_edit_view_alloc(XRemoteApp* app, uint32_t view_id, XRemoteAppButtons* buttons); \ No newline at end of file diff --git a/xremote_learn.c b/xremote_learn.c index 6bbd3b6..4a2c6cf 100644 --- a/xremote_learn.c +++ b/xremote_learn.c @@ -157,12 +157,13 @@ static void xremote_learn_dialog_exit_callback(DialogExResult result, void* cont static uint32_t xremote_learn_text_input_exit_callback(void* context) { TextInput* text_input = context; XRemoteLearnContext* learn_ctx; + XRemoteEvent event; learn_ctx = text_input_get_validator_callback_context(text_input); xremote_app_assert(learn_ctx, XRemoteViewSubmenu); - XRemoteEvent event = learn_ctx->prev_view == XRemoteViewSignal ? XRemoteEventSignalReceived : - XRemoteEventSignalRetry; + event = learn_ctx->prev_view == XRemoteViewSignal ? XRemoteEventSignalReceived : + XRemoteEventSignalRetry; if(learn_ctx->current_button >= XREMOTE_BUTTON_COUNT) learn_ctx->current_button = XREMOTE_BUTTON_COUNT - 1; @@ -193,9 +194,10 @@ static void xremote_learn_text_input_callback(void* context) { snprintf( output_file, sizeof(output_file), - "%s/%s.ir", + "%s/%s%s", XREMOTE_APP_FOLDER, - learn_ctx->text_store); + learn_ctx->text_store, + XREMOTE_APP_EXTENSION); infrared_remote_set_name(learn_ctx->ir_remote, learn_ctx->text_store); infrared_remote_set_path(learn_ctx->ir_remote, output_file); diff --git a/xremote_settings.c b/xremote_settings.c index 39fc5bc..3af88a5 100644 --- a/xremote_settings.c +++ b/xremote_settings.c @@ -13,18 +13,10 @@ typedef struct { XRemoteAppContext* app_ctx; } XRemoteSettingsContext; -#define XREMOTE_ORIENTATION_TEXT_HORIZONTAL "Horizontal" -#define XREMOTE_ORIENTATION_TEXT_VERTICAL "Vertical" #define XREMOTE_ORIENTATION_TEXT "Orientation" -#define XREMOTE_ORIENTATION_INDEX_HORIZONTAL 0 -#define XREMOTE_ORIENTATION_INDEX_VERTICAL 1 #define XREMOTE_ORIENTATION_MAX 2 -#define XREMOTE_EXIT_BEHAVIOR_TEXT_PRESS "Press" -#define XREMOTE_EXIT_BEHAVIOR_TEXT_HOLD "Hold" #define XREMOTE_EXIT_BEHAVIOR_TEXT "Exit Apps" -#define XREMOTE_EXIT_BEHAVIOR_INDEX_PRESS 0 -#define XREMOTE_EXIT_BEHAVIOR_INDEX_HOLD 1 #define XREMOTE_EXIT_BEHAVIOR_MAX 2 #define XREMOTE_REPEAT_TEXT "IR Msg Repeat" @@ -35,45 +27,13 @@ static uint32_t xremote_settings_view_exit_callback(void* context) { return XRemoteViewSubmenu; } -static uint32_t xremote_settings_get_exit_index(XRemoteAppSettings* settings) { - return settings->exit_behavior == XRemoteAppExitPress ? XREMOTE_EXIT_BEHAVIOR_INDEX_PRESS : - XREMOTE_EXIT_BEHAVIOR_INDEX_HOLD; -} - -static const char* xremote_settings_get_exit_str(XRemoteAppSettings* settings) { - return settings->exit_behavior == XRemoteAppExitPress ? XREMOTE_EXIT_BEHAVIOR_TEXT_PRESS : - XREMOTE_EXIT_BEHAVIOR_TEXT_HOLD; -} - -static XRemoteAppExit xremote_settings_get_exit_behavior(uint8_t exit_behavior) { - return exit_behavior == XREMOTE_EXIT_BEHAVIOR_INDEX_PRESS ? XRemoteAppExitPress : - XRemoteAppExitHold; -} - -static uint32_t xremote_settings_get_orientation_index(XRemoteAppSettings* settings) { - return settings->orientation == ViewOrientationHorizontal ? - XREMOTE_ORIENTATION_INDEX_HORIZONTAL : - XREMOTE_ORIENTATION_INDEX_VERTICAL; -} - -static const char* xremote_settings_get_orientation_str(XRemoteAppSettings* settings) { - return settings->orientation == ViewOrientationHorizontal ? - XREMOTE_ORIENTATION_TEXT_HORIZONTAL : - XREMOTE_ORIENTATION_TEXT_VERTICAL; -} - -static ViewOrientation xremote_settings_get_orientation(uint8_t orientation) { - return orientation == XREMOTE_ORIENTATION_INDEX_HORIZONTAL ? ViewOrientationHorizontal : - ViewOrientationVertical; -} - static void infrared_settings_orientation_changed(VariableItem* item) { XRemoteSettingsContext* ctx = variable_item_get_context(item); XRemoteAppSettings* settings = ctx->app_ctx->app_settings; - uint8_t orientation = variable_item_get_current_value_index(item); - settings->orientation = xremote_settings_get_orientation(orientation); - const char* orientation_str = xremote_settings_get_orientation_str(settings); + uint8_t orientation_index = variable_item_get_current_value_index(item); + settings->orientation = xremote_app_get_orientation(orientation_index); + const char* orientation_str = xremote_app_get_orientation_str(settings->orientation); variable_item_set_current_value_text(item, orientation_str); xremote_app_settings_store(settings); @@ -95,9 +55,9 @@ static void infrared_settings_exit_changed(VariableItem* item) { XRemoteSettingsContext* ctx = variable_item_get_context(item); XRemoteAppSettings* settings = ctx->app_ctx->app_settings; - uint8_t exit = variable_item_get_current_value_index(item); - settings->exit_behavior = xremote_settings_get_exit_behavior(exit); - const char* exit_str = xremote_settings_get_exit_str(settings); + uint8_t exit_index = variable_item_get_current_value_index(item); + settings->exit_behavior = xremote_app_get_exit_behavior(exit_index); + const char* exit_str = xremote_app_get_exit_str(settings->exit_behavior); variable_item_set_current_value_text(item, exit_str); xremote_app_settings_store(settings); @@ -125,8 +85,8 @@ static XRemoteSettingsContext* xremote_settings_context_alloc(XRemoteAppContext* context); /* Get application orientation settings */ - const char* orient_str = xremote_settings_get_orientation_str(settings); - uint32_t orient_index = xremote_settings_get_orientation_index(settings); + const char* orient_str = xremote_app_get_orientation_str(settings->orientation); + uint32_t orient_index = xremote_app_get_orientation_index(settings->orientation); /* Set current orientation item index and string */ variable_item_set_current_value_index(item, orient_index); @@ -154,8 +114,8 @@ static XRemoteSettingsContext* xremote_settings_context_alloc(XRemoteAppContext* context); /* Get exit behavior settings */ - const char* exit_str = xremote_settings_get_exit_str(settings); - uint32_t exit_index = xremote_settings_get_exit_index(settings); + const char* exit_str = xremote_app_get_exit_str(settings->exit_behavior); + uint32_t exit_index = xremote_app_get_exit_index(settings->exit_behavior); /* Set exit behavior item index and string */ variable_item_set_current_value_index(item, exit_index); diff --git a/xremote_signal.c b/xremote_signal.c index 91bad7d..948857a 100644 --- a/xremote_signal.c +++ b/xremote_signal.c @@ -55,6 +55,8 @@ static void xremote_signal_receiver_clear_context(XRemoteSignalReceiver* rx_ctx) XRemoteSignalReceiver* xremote_signal_receiver_alloc(XRemoteAppContext* app_ctx) { XRemoteSignalReceiver* rx_ctx = malloc(sizeof(XRemoteSignalReceiver)); + xremote_app_assert(rx_ctx, NULL); + rx_ctx->signal = infrared_signal_alloc(); rx_ctx->worker = infrared_worker_alloc(); diff --git a/xremote_signal.h b/xremote_signal.h index e7c60b2..8eeba5d 100644 --- a/xremote_signal.h +++ b/xremote_signal.h @@ -14,8 +14,9 @@ typedef void (*XRemoteRxCallback)(void* context, InfraredSignal* signal); typedef struct XRemoteSignalReceiver XRemoteSignalReceiver; -XRemoteSignalReceiver* xremote_signal_receiver_alloc(XRemoteAppContext* app_ctx); void xremote_signal_receiver_free(XRemoteSignalReceiver* rx_ctx); +XRemoteSignalReceiver* xremote_signal_receiver_alloc(XRemoteAppContext* app_ctx); +InfraredSignal* xremote_signal_receiver_get_signal(XRemoteSignalReceiver* rx_ctx); void xremote_signal_receiver_set_context( XRemoteSignalReceiver* rx_ctx, @@ -24,10 +25,9 @@ void xremote_signal_receiver_set_context( void xremote_signal_receiver_set_rx_callback( XRemoteSignalReceiver* rx_ctx, XRemoteRxCallback rx_callback); -InfraredSignal* xremote_signal_receiver_get_signal(XRemoteSignalReceiver* rx_ctx); - -void xremote_signal_receiver_start(XRemoteSignalReceiver* rx_ctx); -void xremote_signal_receiver_stop(XRemoteSignalReceiver* rx_ctx); void xremote_signal_receiver_detach(XRemoteSignalReceiver* rx_ctx); void xremote_signal_receiver_attach(XRemoteSignalReceiver* rx_ctx); + +void xremote_signal_receiver_start(XRemoteSignalReceiver* rx_ctx); +void xremote_signal_receiver_stop(XRemoteSignalReceiver* rx_ctx);