Przeglądaj źródła

Add malveke_gb_photo from https://github.com/EstebanFuentealba/MALVEKE-Flipper-Zero

git-subtree-dir: malveke_gb_photo
git-subtree-mainline: b0451b137c8e85e5ddad23921472f78b88cda744
git-subtree-split: b8cd722930bb1dd72ea9c192f9cbe939d03fb79d
Willy-JL 1 rok temu
rodzic
commit
e17e689c6a
47 zmienionych plików z 2647 dodań i 0 usunięć
  1. 191 0
      malveke_gb_photo/.clang-format
  2. BIN
      malveke_gb_photo/.flipcorg/gallery/1.png
  3. BIN
      malveke_gb_photo/.flipcorg/gallery/2.png
  4. BIN
      malveke_gb_photo/.flipcorg/gallery/3.png
  5. BIN
      malveke_gb_photo/.flipcorg/gallery/4.png
  6. 1 0
      malveke_gb_photo/.gitsubtree
  7. 11 0
      malveke_gb_photo/.vscode/settings.json
  8. 129 0
      malveke_gb_photo/README.md
  9. 40 0
      malveke_gb_photo/README_catalog.md
  10. 21 0
      malveke_gb_photo/application.fam
  11. 16 0
      malveke_gb_photo/docs/changelog.md
  12. BIN
      malveke_gb_photo/docs/images/flipper-zero-flat-01.png
  13. BIN
      malveke_gb_photo/docs/images/flipper-zero-flat-02.png
  14. BIN
      malveke_gb_photo/docs/images/flipper-zero-flat-03.png
  15. BIN
      malveke_gb_photo/docs/images/flipper-zero-flat-04.png
  16. BIN
      malveke_gb_photo/docs/images/flipper-zero-flat-05.png
  17. BIN
      malveke_gb_photo/docs/images/flipper-zero-flat-06.png
  18. BIN
      malveke_gb_photo/docs/images/flipper-zero-flat-07.png
  19. BIN
      malveke_gb_photo/docs/images/flipper-zero-flat-08.png
  20. BIN
      malveke_gb_photo/docs/images/flipper-zero-flat-09.png
  21. BIN
      malveke_gb_photo/docs/images/flipper-zero-flat-10.png
  22. BIN
      malveke_gb_photo/docs/psd/gb_photo.psd
  23. 121 0
      malveke_gb_photo/helpers/bmp.c
  24. 12 0
      malveke_gb_photo/helpers/bmp.h
  25. 61 0
      malveke_gb_photo/helpers/boilerplate_custom_event.h
  26. 17 0
      malveke_gb_photo/helpers/malveke_photo.c
  27. 10 0
      malveke_gb_photo/helpers/malveke_photo.h
  28. BIN
      malveke_gb_photo/icons/arrow_13x6.png
  29. BIN
      malveke_gb_photo/icons/icon_10px.png
  30. 148 0
      malveke_gb_photo/malveke_gb_photo.c
  31. 141 0
      malveke_gb_photo/malveke_gb_photo.h
  32. 30 0
      malveke_gb_photo/scenes/boilerplate_scene.c
  33. 29 0
      malveke_gb_photo/scenes/boilerplate_scene.h
  34. 8 0
      malveke_gb_photo/scenes/boilerplate_scene_config.h
  35. 87 0
      malveke_gb_photo/scenes/boilerplate_scene_menu.c
  36. 50 0
      malveke_gb_photo/scenes/boilerplate_scene_scene_1.c
  37. 50 0
      malveke_gb_photo/scenes/boilerplate_scene_scene_2.c
  38. 515 0
      malveke_gb_photo/scenes/boilerplate_scene_settings.c
  39. 55 0
      malveke_gb_photo/scenes/boilerplate_scene_startscreen.c
  40. 54 0
      malveke_gb_photo/scenes/scene_file_select.c
  41. 77 0
      malveke_gb_photo/u8g2_font_5x7_mf.h
  42. 339 0
      malveke_gb_photo/views/boilerplate_scene_1.c
  43. 30 0
      malveke_gb_photo/views/boilerplate_scene_1.h
  44. 247 0
      malveke_gb_photo/views/boilerplate_scene_2.c
  45. 22 0
      malveke_gb_photo/views/boilerplate_scene_2.h
  46. 116 0
      malveke_gb_photo/views/boilerplate_startscreen.c
  47. 19 0
      malveke_gb_photo/views/boilerplate_startscreen.h

+ 191 - 0
malveke_gb_photo/.clang-format

@@ -0,0 +1,191 @@
+---
+Language:        Cpp
+AccessModifierOffset: -4
+AlignAfterOpenBracket: AlwaysBreak
+AlignArrayOfStructures: None
+AlignConsecutiveMacros: None
+AlignConsecutiveAssignments: None
+AlignConsecutiveBitFields: None
+AlignConsecutiveDeclarations: None
+AlignEscapedNewlines: Left
+AlignOperands:   Align
+AlignTrailingComments: false
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowShortEnumsOnASingleLine: true
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortLambdasOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: WithoutElse
+AllowShortLoopsOnASingleLine: true
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: Yes
+AttributeMacros:
+  - __capability
+BinPackArguments: false
+BinPackParameters: false
+BraceWrapping:
+  AfterCaseLabel:  false
+  AfterClass:      false
+  AfterControlStatement: Never
+  AfterEnum:       false
+  AfterFunction:   false
+  AfterNamespace:  false
+  AfterObjCDeclaration: false
+  AfterStruct:     false
+  AfterUnion:      false
+  AfterExternBlock: false
+  BeforeCatch:     false
+  BeforeElse:      false
+  BeforeLambdaBody: false
+  BeforeWhile:     false
+  IndentBraces:    false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: true
+BreakBeforeBraces: Attach
+BreakBeforeInheritanceComma: false
+BreakInheritanceList: BeforeColon
+BreakBeforeTernaryOperators: false
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeComma
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: false
+ColumnLimit:     99
+CommentPragmas:  '^ IWYU pragma:'
+QualifierAlignment: Leave
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DeriveLineEnding: true
+DerivePointerAlignment: false
+DisableFormat:   false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+ExperimentalAutoDetectBinPacking: false
+PackConstructorInitializers: BinPack
+BasedOnStyle:    ''
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+AllowAllConstructorInitializersOnNextLine: true
+FixNamespaceComments: false
+ForEachMacros:
+  - foreach
+  - Q_FOREACH
+  - BOOST_FOREACH
+IfMacros:
+  - KJ_IF_MAYBE
+IncludeBlocks:   Preserve
+IncludeCategories:
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '^(<|"(gtest|gmock|isl|json)/)'
+    Priority:        3
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseLabels: false
+IndentCaseBlocks: false
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentExternBlock: AfterExternBlock
+IndentRequires:  false
+IndentWidth:     4
+IndentWrappedFunctionNames: true
+InsertTrailingCommas: None
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+LambdaBodyIndentation: Signature
+MacroBlockBegin: ''
+MacroBlockEnd:   ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 4
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: true
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakAssignment: 10
+PenaltyBreakBeforeFirstCallParameter: 30
+PenaltyBreakComment: 10
+PenaltyBreakFirstLessLess: 0
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakString: 10
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 100
+PenaltyReturnTypeOnItsOwnLine: 60
+PenaltyIndentedWhitespace: 0
+PointerAlignment: Left
+PPIndentWidth:   -1
+ReferenceAlignment: Pointer
+ReflowComments:  false
+RemoveBracesLLVM: false
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SortIncludes:    Never
+SortJavaStaticImport: Before
+SortUsingDeclarations: false
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: Never
+SpaceBeforeParensOptions:
+  AfterControlStatements: false
+  AfterForeachMacros: false
+  AfterFunctionDefinitionName: false
+  AfterFunctionDeclarationName: false
+  AfterIfMacros:   false
+  AfterOverloadedOperator: false
+  BeforeNonEmptyParentheses: false
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyBlock: false
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles:  Never
+SpacesInConditionalStatement: false
+SpacesInContainerLiterals: false
+SpacesInCStyleCastParentheses: false
+SpacesInLineCommentPrefix:
+  Minimum:         1
+  Maximum:         -1
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+SpaceBeforeSquareBrackets: false
+BitFieldColonSpacing: Both
+Standard:        c++03
+StatementAttributeLikeMacros:
+  - Q_EMIT
+StatementMacros:
+  - Q_UNUSED
+  - QT_REQUIRE_VERSION
+TabWidth:        4
+UseCRLF:         false
+UseTab:          Never
+WhitespaceSensitiveMacros:
+  - STRINGIZE
+  - PP_STRINGIZE
+  - BOOST_PP_STRINGIZE
+  - NS_SWIFT_NAME
+  - CF_SWIFT_NAME
+...
+

BIN
malveke_gb_photo/.flipcorg/gallery/1.png


BIN
malveke_gb_photo/.flipcorg/gallery/2.png


BIN
malveke_gb_photo/.flipcorg/gallery/3.png


BIN
malveke_gb_photo/.flipcorg/gallery/4.png


+ 1 - 0
malveke_gb_photo/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/EstebanFuentealba/MALVEKE-Flipper-Zero main flipper_companion_apps/applications/external/malveke_gb_photo

+ 11 - 0
malveke_gb_photo/.vscode/settings.json

@@ -0,0 +1,11 @@
+{
+    "files.associations": {
+        "malveke_gb_photo.h": "c",
+        "boilerplate_custom_event.h": "c",
+        "boilerplate_scene_1.h": "c",
+        "boilerplate_startscreen.h": "c",
+        "boilerplate_scene.h": "c",
+        "boilerplate_storage.h": "c",
+        "u8g2_font_5x7_mf.h": "c"
+    }
+}

+ 129 - 0
malveke_gb_photo/README.md

@@ -0,0 +1,129 @@
+# ***GAME BOY*** PHOTO MALVEKE
+
+<div align="center">
+
+**Official** | **Unleashed** | **RogueMaster** | **Xtreme**
+:- | :- | :- | :- 
+[![FlipC.org](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero/badge?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_photo&firmware=official)](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_photo&firmware=official)|[![FlipC.org](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero/badge?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_photo&firmware=unleashed)](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_photo&firmware=unleashed)|[![FlipC.org](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero/badge?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_photo&firmware=roguemaster)](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_photo&firmware=roguemaster)|[![FlipC.org](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero/badge?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_photo&firmware=xtreme)](https://flipc.org/EstebanFuentealba/MALVEKE-Flipper-Zero?branch=main&root=flipper_companion_apps%2Fapplications%2Fexternal%2Fmalveke_gb_photo&firmware=xtreme)
+</div>
+<br>
+
+<p align='center'>
+<a href="https://www.tindie.com/stores/efuentealba/?ref=offsite_badges&utm_source=sellers_efuentealba&utm_medium=badges&utm_campaign=badge_large"><img src="https://d2ss6ovg47m0r5.cloudfront.net/badges/tindie-larges.png" alt="I sell on Tindie" width="200" height="104"></a>
+</p>
+
+## Introduction
+
+## Features
+- Extract "large" photos of 4 colors (128x112) into .BMP format.
+- Extract a single photo or batch extract all photos.
+- Implement a Photo Viewer within Flipper Zero.
+- Restore deleted photos.
+- Modify the Color Palette.
+
+
+
+## Instructions for use.
+
+These instructions assume that you are starting at the Flipper Zero desktop. Otherwise, press the Back button until you are at the desktop.
+
+- Press the `OK` button on the Flipper to open the main menu.
+- Choose `Applications` from the menu.
+- Choose `GPIO` from the submenu.
+- Choose `[GB] GAME BOY PHOTO`
+
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-01.png" width="400" />
+        <br />
+    </p>
+
+- For the next step, you'll need a `.SAV` file extracted from the GAME BOY Camera's RAM. If you don't have one, you can use the [**GAME BOY Cartridge (GB/GBC) MALVEKE**](/flipper_companion_apps/applications/external/malveke_gb_cartridge/README.md#instructions-for-use) app to save the RAM.
+
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-02.png" width="400" />
+        <br />
+    </p>
+
+- Press the `OK` to enter to menu, After selecting the `.SAV` file, it will display the main menu. 
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-03.png" width="400" />
+        <br />
+    </p>
+- **Photo Viewer**, a simple image viewer where you navigate between photos using `LEFT`/`RIGHT`. Since the images are 128x112, you scroll vertically using the `UP` and `DOWN` keys.
+
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-04.png" width="400" />
+        <br />
+    </p>
+
+    - You can save the currently displayed image to the Flipper Zero's SD card by pressing the `OK` button. The image will be saved in the `SD Card/apps_data/malveke/photos` folder. 
+
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-05.png" width="400" />
+        <br />
+    </p>
+
+
+- **Export all** allows you to export all the images. Simply press the `OK` button and wait for the transfer of all images. 
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-06.png" width="400" />
+        <br />
+    </p>
+
+    - The images will be saved in the `SD Card/apps_data/malveke/photos` folder. 
+
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-07.png" width="400" />
+        <br />
+    </p>
+
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-08.png" width="400" />
+        <br />
+    </p>
+- **Settings**: General application configurations.
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-09.png" width="400" />
+        <br />
+    </p>
+
+    - `Palette`: Configure the color palette; there are over 50 palettes to choose from. By default, it's set to Black and White.
+    - `Info`: displays which images have been deleted.
+
+    <p align='center'>
+        <br />
+        <img src="./docs/images/flipper-zero-flat-10.png" width="400" />
+        <br />
+    </p>
+
+- Press the `Back` to back to principal menu.
+
+
+## TODO
+- [ ] Refactor Code
+- [ ] Reading images directly from the cartridge (without opening a .SAV file).
+- [ ] For BMP, avoid using memory to generate images; write directly to files.
+- [ ] Exhibit and view images through a Web Server interface.
+- [ ] Add frames to images.
+- [ ] Configure dimension upscaling.
+- [ ] Export to other file formats.
+- [ ] Extract Game Faces.
+
+## Acknowledgements
+- [**@raphnet**](https://github.com/raphnet/gbcam2png/) command-line tool for extracting Game Boy Camera photos from a save ram (.sav) file and writing them in the PNG format.
+- [**@HerrZatacke**](https://github.com/HerrZatacke/gb-printer-web/) Several ideas drawn from your Gameboy Printer Web.
+
+<p align='center'>
+<br />
+<br />
+From Talcahuano 🇨🇱 with ❤ 
+</p>

+ 40 - 0
malveke_gb_photo/README_catalog.md

@@ -0,0 +1,40 @@
+# ***GAME BOY*** PHOTO MALVEKE
+
+
+## Introduction
+
+## Features
+- Extract "large" photos of 4 colors (128x112) into .BMP format.
+- Extract a single photo or batch extract all photos.
+- Implement a Photo Viewer within Flipper Zero.
+- Restore deleted photos.
+- Modify the Color Palette.
+
+## Instructions for use.
+
+These instructions assume that you are starting at the Flipper Zero desktop. Otherwise, press the Back button until you are at the desktop.
+
+- Press the **OK** button on the Flipper to open the main menu.
+- Choose **Applications** from the menu.
+- Choose **GPIO** from the submenu.
+- Choose **GB GAME BOY PHOTO**
+- For the next step, you'll need a **.SAV** file extracted from the GAME BOY Camera's RAM. If you don't have one, you can use the [**MALVEKE GAME BOY Cartridge (GB/GBC)**](https://github.com/EstebanFuentealba/MALVEKE-Flipper-Zero/blob/main/flipper_companion_apps/applications/external/malveke_gb_cartridge/README.md#instructions-for-use) app to save the RAM.
+- Press the **OK** to enter to menu, After selecting the **.SAV** file, it will display the main menu. 
+- **Photo Viewer**, a simple image viewer where you navigate between photos using **LEFT**/**RIGHT**. Since the images are 128x112, you scroll vertically using the **UP** and **DOWN** keys.
+    - You can save the currently displayed image to the Flipper Zero's SD card by pressing the **OK** button. The image will be saved in the **SD Card/apps_data/malveke/photos** folder. 
+- **Export all** allows you to export all the images. Simply press the **OK** button and wait for the transfer of all images. 
+    - The images will be saved in the **SD Card/apps_data/malveke/photos** folder. 
+- **Settings**: General application configurations.
+    - **Palette**: Configure the color palette; there are over 50 palettes to choose from. By default, it's set to Black and White.
+    - **Info**: displays which images have been deleted.
+- Press the **Back** to back to principal menu.
+
+
+## TODO
+- Refactor Code
+- For BMP, avoid using memory to generate images; write directly to files.
+- Exhibit and view images through a Web Server interface.
+- Add frames to images.
+- Configure dimension upscaling.
+- Export to other file formats.
+- Extract Game Faces.

+ 21 - 0
malveke_gb_photo/application.fam

@@ -0,0 +1,21 @@
+App(
+    appid="malveke_gb_photo",
+    name="GAME BOY PHOTO MALVEKE",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="boilerplate_app",
+    cdefines=["APP_GB_PHOTO"],
+    requires=[
+        "gui",
+        "storage",
+    ],
+    stack_size=2 * 1024,
+    order=10,
+    fap_version=[2,3],
+    fap_libs=["assets"],
+    fap_icon="icons/icon_10px.png",
+    fap_icon_assets="icons",
+    fap_category="GPIO",
+    fap_description="Game Boy Camera save RAM photo to BMP from the Flipper Zero.",
+	fap_author="Esteban Fuentealba",
+	fap_weburl="https://github.com/EstebanFuentealba/MALVEKE-Flipper-Zero/"
+)

+ 16 - 0
malveke_gb_photo/docs/changelog.md

@@ -0,0 +1,16 @@
+# Changelog - Patch Notes
+
+## Version 2.3
+Memory optimization and fix for issue #35.
+
+## Version 2.2
+Fix Frozen upon exiting the gb photo.
+
+## Version 2.1
+Fix FuriHalRtc: new datetime lib.
+
+## Version 2.0
+Implement new serial API with new module expansion protocol.
+
+## Version 1.0
+The initial version of the application includes features to view and export images stored on a GAME BOY Camera cartridge.

BIN
malveke_gb_photo/docs/images/flipper-zero-flat-01.png


BIN
malveke_gb_photo/docs/images/flipper-zero-flat-02.png


BIN
malveke_gb_photo/docs/images/flipper-zero-flat-03.png


BIN
malveke_gb_photo/docs/images/flipper-zero-flat-04.png


BIN
malveke_gb_photo/docs/images/flipper-zero-flat-05.png


BIN
malveke_gb_photo/docs/images/flipper-zero-flat-06.png


BIN
malveke_gb_photo/docs/images/flipper-zero-flat-07.png


BIN
malveke_gb_photo/docs/images/flipper-zero-flat-08.png


BIN
malveke_gb_photo/docs/images/flipper-zero-flat-09.png


BIN
malveke_gb_photo/docs/images/flipper-zero-flat-10.png


BIN
malveke_gb_photo/docs/psd/gb_photo.psd


+ 121 - 0
malveke_gb_photo/helpers/bmp.c

@@ -0,0 +1,121 @@
+#include "bmp.h"
+void bmp_init(void* buf, long width, long height) {
+    long pad;
+    unsigned long size;
+    unsigned long uw = width;
+    unsigned long uh = -height;
+    unsigned char* p = (unsigned char*)buf;
+
+#ifdef BMP_COMPAT
+    uh = height;
+#endif
+
+    /* bfType */
+    *p++ = 0x42;
+    *p++ = 0x4D;
+
+    /* bfSize */
+    pad = (width * -3UL) & 3;
+    size = height * (width * 3 + pad) + 14 + 40;
+    *p++ = size >> 0;
+    *p++ = size >> 8;
+    *p++ = size >> 16;
+    *p++ = size >> 24;
+
+    /* bfReserved1 + bfReserved2 */
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+
+    /* bfOffBits */
+    *p++ = 0x36;
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+
+    /* biSize */
+    *p++ = 0x28;
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+
+    /* biWidth */
+    *p++ = uw >> 0;
+    *p++ = uw >> 8;
+    *p++ = uw >> 16;
+    *p++ = uw >> 24;
+
+    /* biHeight */
+    *p++ = uh >> 0;
+    *p++ = uh >> 8;
+    *p++ = uh >> 16;
+    *p++ = uh >> 24;
+
+    /* biPlanes */
+    *p++ = 0x01;
+    *p++ = 0x00;
+
+    /* biBitCount */
+    *p++ = 0x18;
+    *p++ = 0x00;
+
+    /* biCompression */
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+
+    /* biSizeImage */
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+
+    /* biXPelsPerMeter */
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+
+    /* biYPelsPerMeter */
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+
+    /* biClrUsed */
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+
+    /* biClrImportant */
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p++ = 0x00;
+    *p = 0x00;
+}
+void bmp_set(void* buf, long x, long y, unsigned long color) {
+    unsigned char* p;
+    unsigned char* hdr = (unsigned char*)buf;
+    unsigned long width = (unsigned long)hdr[18] << 0 | (unsigned long)hdr[19] << 8 |
+                          (unsigned long)hdr[20] << 16 | (unsigned long)hdr[21] << 24;
+    long pad = (width * -3UL) & 3;
+#ifdef BMP_COMPAT
+    unsigned long height = (unsigned long)hdr[22] << 0 | (unsigned long)hdr[23] << 8 |
+                           (unsigned long)hdr[24] << 16 | (unsigned long)hdr[25] << 24;
+    y = height - y - 1;
+#endif
+    p = hdr + 14 + 40 + y * (width * 3 + pad) + x * 3;
+    p[0] = color >> 0;
+    p[1] = color >> 8;
+    p[2] = color >> 16;
+}
+unsigned long bmp_encode(unsigned long color_hex) {
+    unsigned char r = (color_hex >> 16) & 0xFF;
+    unsigned char g = (color_hex >> 8) & 0xFF;
+    unsigned char b = color_hex & 0xFF;
+
+    return b | g << 8 | r << 16;
+}

+ 12 - 0
malveke_gb_photo/helpers/bmp.h

@@ -0,0 +1,12 @@
+#pragma once
+
+#ifndef BMP_H
+#define BMP_H
+
+#define BMP_SIZE(w, h) ((h) * ((w) * 3 + (((w) * -3UL) & 3)) + 14 + 40)
+
+void bmp_init(void* buf, long width, long height);
+void bmp_set(void* buf, long x, long y, unsigned long color);
+unsigned long bmp_encode(unsigned long color_hex);
+
+#endif /* BMP_H */

+ 61 - 0
malveke_gb_photo/helpers/boilerplate_custom_event.h

@@ -0,0 +1,61 @@
+#pragma once
+
+typedef enum {
+    BoilerplateCustomEventStartscreenUp,
+    BoilerplateCustomEventStartscreenDown,
+    BoilerplateCustomEventStartscreenLeft,
+    BoilerplateCustomEventStartscreenRight,
+    BoilerplateCustomEventStartscreenOk,
+    BoilerplateCustomEventStartscreenBack,
+    BoilerplateCustomEventScene1Up,
+    BoilerplateCustomEventScene1Down,
+    BoilerplateCustomEventScene1Left,
+    BoilerplateCustomEventScene1Right,
+    BoilerplateCustomEventScene1Ok,
+    BoilerplateCustomEventScene1Back,
+    BoilerplateCustomEventScene2Up,
+    BoilerplateCustomEventScene2Down,
+    BoilerplateCustomEventScene2Left,
+    BoilerplateCustomEventScene2Right,
+    BoilerplateCustomEventScene2Ok,
+    BoilerplateCustomEventScene2Back,
+} BoilerplateCustomEvent;
+
+enum BoilerplateCustomEventType {
+    // Reserve first 100 events for button types and indexes, starting from 0
+    BoilerplateCustomEventMenuVoid,
+    BoilerplateCustomEventMenuSelected,
+};
+
+#pragma pack(push, 1)
+typedef union {
+    uint32_t packed_value;
+    struct {
+        uint16_t type;
+        int16_t value;
+    } content;
+} BoilerplateCustomEventMenu;
+#pragma pack(pop)
+
+static inline uint32_t boilerplate_custom_menu_event_pack(uint16_t type, int16_t value) {
+    BoilerplateCustomEventMenu event = {.content = {.type = type, .value = value}};
+    return event.packed_value;
+}
+static inline void
+    boilerplate_custom_menu_event_unpack(uint32_t packed_value, uint16_t* type, int16_t* value) {
+    BoilerplateCustomEventMenu event = {.packed_value = packed_value};
+    if(type) *type = event.content.type;
+    if(value) *value = event.content.value;
+}
+
+static inline uint16_t boilerplate_custom_menu_event_get_type(uint32_t packed_value) {
+    uint16_t type;
+    boilerplate_custom_menu_event_unpack(packed_value, &type, NULL);
+    return type;
+}
+
+static inline int16_t boilerplate_custom_menu_event_get_value(uint32_t packed_value) {
+    int16_t value;
+    boilerplate_custom_menu_event_unpack(packed_value, NULL, &value);
+    return value;
+}

+ 17 - 0
malveke_gb_photo/helpers/malveke_photo.c

@@ -0,0 +1,17 @@
+#include "malveke_photo.h"
+
+void get_timefilename(FuriString* name, int index) {
+    DateTime datetime = {0};
+    furi_hal_rtc_get_datetime(&datetime);
+    furi_string_printf(
+        name,
+        "%s/%.4d%.2d%.2d-%.2d%.2d%.2d-%d.bmp",
+        MALVEKE_APP_FOLDER_PHOTOS,
+        datetime.year,
+        datetime.month,
+        datetime.day,
+        datetime.hour,
+        datetime.minute,
+        datetime.second,
+        index);
+}

+ 10 - 0
malveke_gb_photo/helpers/malveke_photo.h

@@ -0,0 +1,10 @@
+#pragma once
+
+#ifndef MALVEKE_PHOTO_H
+#define MALVEKE_PHOTO_H
+
+#include "../malveke_gb_photo.h"
+
+void get_timefilename(FuriString* name, int index);
+
+#endif /* MALVEKE_PHOTO_H */

BIN
malveke_gb_photo/icons/arrow_13x6.png


BIN
malveke_gb_photo/icons/icon_10px.png


+ 148 - 0
malveke_gb_photo/malveke_gb_photo.c

@@ -0,0 +1,148 @@
+#include "malveke_gb_photo.h"
+#include "u8g2_font_5x7_mf.h"
+
+bool boilerplate_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    Boilerplate* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+void boilerplate_tick_event_callback(void* context) {
+    furi_assert(context);
+    Boilerplate* app = context;
+    scene_manager_handle_tick_event(app->scene_manager);
+}
+
+//leave app if back button pressed
+bool boilerplate_navigation_event_callback(void* context) {
+    furi_assert(context);
+    Boilerplate* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+Boilerplate* boilerplate_app_alloc() {
+    Boilerplate* app = malloc(sizeof(Boilerplate));
+    app->gui = furi_record_open(RECORD_GUI);
+    app->notification = furi_record_open(RECORD_NOTIFICATION);
+    app->storage = furi_record_open(RECORD_STORAGE);
+
+    //Turn backlight on, believe me this makes testing your app easier
+    notification_message(app->notification, &sequence_display_backlight_on);
+
+    //Scene additions
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+
+    app->scene_manager = scene_manager_alloc(&boilerplate_scene_handlers, app);
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, boilerplate_navigation_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, boilerplate_tick_event_callback, 100);
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, boilerplate_custom_event_callback);
+    app->submenu = submenu_alloc();
+
+    app->palette = BoilerplatePaletteBlackAndWhite;
+    app->info = BoilerplateInfoOn;
+    app->palette_color_hex_a = 0xFFFFFF;
+    app->palette_color_hex_b = 0xAAAAAA;
+    app->palette_color_hex_c = 0x555555;
+    app->palette_color_hex_d = 0x000000;
+
+    app->file_path = furi_string_alloc();
+    app->camera_ram_sav = storage_file_alloc(app->storage);
+    app->page = 0;
+    app->pos_x = 0;
+    app->pos_y = 0;
+    app->show_instructions = true;
+
+    // Used for File Browser
+    app->dialogs = furi_record_open(RECORD_DIALOGS);
+    app->file_path = furi_string_alloc();
+
+    view_dispatcher_add_view(
+        app->view_dispatcher, BoilerplateViewIdMenu, submenu_get_view(app->submenu));
+    app->boilerplate_startscreen = boilerplate_startscreen_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        BoilerplateViewIdStartscreen,
+        boilerplate_startscreen_get_view(app->boilerplate_startscreen));
+    app->boilerplate_scene_1 = boilerplate_scene_1_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        BoilerplateViewIdScene1,
+        boilerplate_scene_1_get_view(app->boilerplate_scene_1));
+    app->boilerplate_scene_2 = boilerplate_scene_2_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        BoilerplateViewIdScene2,
+        boilerplate_scene_2_get_view(app->boilerplate_scene_2));
+
+    app->variable_item_list = variable_item_list_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        BoilerplateViewIdSettings,
+        variable_item_list_get_view(app->variable_item_list));
+
+    //End Scene Additions
+
+    return app;
+}
+
+void boilerplate_app_free(Boilerplate* app) {
+    furi_assert(app);
+
+    // Views
+    view_dispatcher_remove_view(app->view_dispatcher, BoilerplateViewIdStartscreen);
+    view_dispatcher_remove_view(app->view_dispatcher, BoilerplateViewIdMenu);
+    view_dispatcher_remove_view(app->view_dispatcher, BoilerplateViewIdScene1);
+    view_dispatcher_remove_view(app->view_dispatcher, BoilerplateViewIdScene2);
+    view_dispatcher_remove_view(app->view_dispatcher, BoilerplateViewIdSettings);
+
+    submenu_free(app->submenu);
+
+    // View Dispatcher
+    view_dispatcher_free(app->view_dispatcher);
+
+    // Scene manager
+    scene_manager_free(app->scene_manager);
+
+    if(app->camera_ram_sav && storage_file_is_open(app->camera_ram_sav)) {
+        storage_file_close(app->camera_ram_sav);
+        storage_file_free(app->camera_ram_sav);
+    }
+
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_STORAGE);
+
+    app->gui = NULL;
+    app->notification = NULL;
+    app->storage = NULL;
+
+    // Close File Browser
+    furi_record_close(RECORD_DIALOGS);
+    furi_string_free(app->file_path);
+
+    //Remove whatever is left
+    free(app);
+}
+
+int32_t boilerplate_app(void* p) {
+    UNUSED(p);
+    Boilerplate* app = boilerplate_app_alloc();
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    scene_manager_next_scene(
+        app->scene_manager, BoilerplateSceneStartscreen); //Start with start screen
+
+    furi_hal_power_suppress_charge_enter();
+
+    view_dispatcher_run(app->view_dispatcher);
+
+    furi_hal_power_suppress_charge_exit();
+    boilerplate_app_free(app);
+
+    return 0;
+}

+ 141 - 0
malveke_gb_photo/malveke_gb_photo.h

@@ -0,0 +1,141 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <input/input.h>
+#include <stdlib.h>
+#include <malveke_gb_photo_icons.h>
+#include <dialogs/dialogs.h>
+#include <notification/notification_messages.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/submenu.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/dialog_ex.h>
+#include <storage/storage.h>
+#include "scenes/boilerplate_scene.h"
+#include "views/boilerplate_startscreen.h"
+#include "views/boilerplate_scene_1.h"
+#include "views/boilerplate_scene_2.h"
+
+#include <string.h>
+
+#define TAG "GBPHOTO"
+
+#define MALVEKE_APP_RAM_EXTENSION ".sav"
+
+#define MALVEKE_APP_FOLDER_USER "apps_data/malveke"
+#define MALVEKE_APP_FOLDER EXT_PATH(MALVEKE_APP_FOLDER_USER)
+#define MALVEKE_APP_FOLDER_RAMS MALVEKE_APP_FOLDER "/rams"
+#define MALVEKE_APP_FOLDER_PHOTOS MALVEKE_APP_FOLDER "/photos"
+
+#define GB_FIRST_PHOTO_OFFSET 0x2000
+#define GB_PHOTO_SIZE 0x1000
+
+typedef struct {
+    Gui* gui;
+    NotificationApp* notification;
+    Storage* storage;
+    ViewDispatcher* view_dispatcher;
+    Submenu* submenu;
+    SceneManager* scene_manager;
+    VariableItemList* variable_item_list;
+    BoilerplateStartscreen* boilerplate_startscreen;
+    BoilerplateScene1* boilerplate_scene_1;
+    BoilerplateScene2* boilerplate_scene_2;
+    DialogsApp* dialogs; // File Browser
+    FuriString* file_path; // File Browser
+    uint32_t palette;
+    uint32_t info;
+    File* camera_ram_sav;
+    int page;
+    int pos_x;
+    int pos_y;
+    bool show_instructions;
+    uint8_t tile_data[16];
+
+    unsigned long palette_color_hex_a;
+    unsigned long palette_color_hex_b;
+    unsigned long palette_color_hex_c;
+    unsigned long palette_color_hex_d;
+
+} Boilerplate;
+
+typedef enum {
+    WorkerEvtStop = (1 << 0),
+    WorkerEvtRxDone = (1 << 1),
+} WorkerEvtFlags;
+
+typedef enum {
+    BoilerplateViewIdStartscreen,
+    BoilerplateViewIdMenu,
+    BoilerplateViewIdScene1,
+    BoilerplateViewIdScene2,
+    BoilerplateViewIdSettings,
+} BoilerplateViewId;
+
+typedef enum {
+    BoilerplatePaletteBlackAndWhite,
+    BoilerplatePaletteOriginal,
+    BoilerplatePaletteSplashUp,
+    BoilerplatePaletteGBLight,
+    BoilerplatePalettePocket,
+    BoilerplatePaletteAudiQuattroPikesPeak,
+    BoilerplatePaletteAzureClouds,
+    BoilerplatePaletteTheresalwaysmoney,
+    BoilerplatePaletteBGBEmulator,
+    BoilerplatePaletteGameBoyBlackZeropalette,
+    BoilerplatePaletteCandyCottonTowerRaid,
+    BoilerplatePaletteCaramelFudgeParanoia,
+    BoilerplatePaletteCGAPaletteCrush1,
+    BoilerplatePaletteCGAPaletteCrush2,
+    BoilerplatePaletteChildhoodinGreenland,
+    BoilerplatePaletteCMYKeystone,
+    BoilerplatePaletteCyanideBlues,
+    BoilerplatePaletteDune2000remastered,
+    BoilerplatePaletteDrowningatnight,
+    BoilerplatePaletteDeepHazeGreen,
+    BoilerplatePaletteDiesistmeineWassermelone,
+    BoilerplatePaletteFlowerfeldstrabe,
+    BoilerplatePaletteFloydSteinberginLove,
+    BoilerplatePaletteGameBoyColorSplashDown,
+    BoilerplatePaletteGameBoyColorSplashDownA,
+    BoilerplatePaletteGameBoyColorSplashDownB,
+    BoilerplatePaletteGameBoyColorSplashRightAGameBoyCamera,
+    BoilerplatePaletteGameBoyColorSplashLeft,
+    BoilerplatePaletteGameBoyColorSplashLeftA,
+    BoilerplatePaletteGameBoyColorSplashLeftB,
+    BoilerplatePaletteGameBoyColorSplashRight,
+    BoilerplatePaletteGameBoyColorSplashRightB,
+    BoilerplatePaletteGameBoyColorSplashUpA,
+    BoilerplatePaletteGameBoyColorSplashUpB,
+    BoilerplatePaletteGoldenElephantCurry,
+    BoilerplatePaletteGlowingMountains,
+    BoilerplatePaletteGrafixkidGray,
+    BoilerplatePaletteGrafixkidGreen,
+    BoilerplatePaletteArtisticCaffeinatedLactose,
+    BoilerplatePaletteKneeDeepintheWood,
+    BoilerplatePaletteLinkslateAwakening,
+    BoilerplatePaletteMetroidAranremixed,
+    BoilerplatePaletteNortoriousComandante,
+    BoilerplatePalettePurpleRain,
+    BoilerplatePaletteRustedCitySign,
+    BoilerplatePaletteRomerosGarden,
+    BoilerplatePaletteSunflowerHolidays,
+    BoilerplatePaletteSuperHyperMegaGameboy,
+    BoilerplatePaletteSpaceHazeOverload,
+    BoilerplatePaletteStarlitMemories,
+    BoilerplatePaletteMyFriendfromBavaria,
+    BoilerplatePaletteThedeathofYungColumbus,
+    BoilerplatePaletteTramontoalParcodegliAcquedotti,
+    BoilerplatePaletteThestarryknight,
+    BoilerplatePaletteVirtualBoy1985,
+    BoilerplatePaletteWaterfrontPlaza,
+    BoilerplatePaletteYouthIkarusreloaded
+} BoilerplatePaletteState;
+
+typedef enum {
+    BoilerplateInfoOff,
+    BoilerplateInfoOn,
+} BoilerplateInfoState;

+ 30 - 0
malveke_gb_photo/scenes/boilerplate_scene.c

@@ -0,0 +1,30 @@
+#include "boilerplate_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const boilerplate_on_enter_handlers[])(void*) = {
+#include "boilerplate_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const boilerplate_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "boilerplate_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const boilerplate_on_exit_handlers[])(void* context) = {
+#include "boilerplate_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers boilerplate_scene_handlers = {
+    .on_enter_handlers = boilerplate_on_enter_handlers,
+    .on_event_handlers = boilerplate_on_event_handlers,
+    .on_exit_handlers = boilerplate_on_exit_handlers,
+    .scene_num = BoilerplateSceneNum,
+};

+ 29 - 0
malveke_gb_photo/scenes/boilerplate_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) BoilerplateScene##id,
+typedef enum {
+#include "boilerplate_scene_config.h"
+    BoilerplateSceneNum,
+} BoilerplateScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers boilerplate_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "boilerplate_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
+#include "boilerplate_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
+#include "boilerplate_scene_config.h"
+#undef ADD_SCENE

+ 8 - 0
malveke_gb_photo/scenes/boilerplate_scene_config.h

@@ -0,0 +1,8 @@
+ADD_SCENE(boilerplate, startscreen, Startscreen)
+ADD_SCENE(boilerplate, fileselect, Fileselect)
+ADD_SCENE(boilerplate, menu, Menu)
+ADD_SCENE(boilerplate, scene_1, Scene_1)
+ADD_SCENE(boilerplate, scene_2, Scene_2)
+// ADD_SCENE(boilerplate, scene_3, Scene_3)
+// ADD_SCENE(boilerplate, scene_4, Scene_4)
+ADD_SCENE(boilerplate, settings, Settings)

+ 87 - 0
malveke_gb_photo/scenes/boilerplate_scene_menu.c

@@ -0,0 +1,87 @@
+#include "../malveke_gb_photo.h"
+
+enum SubmenuIndex {
+    SubmenuIndexScene1 = 10,
+    SubmenuIndexScene2,
+    SubmenuIndexScene3,
+    SubmenuIndexScene4,
+    SubmenuIndexScene5,
+    SubmenuIndexSettings,
+};
+
+void boilerplate_scene_menu_submenu_callback(void* context, uint32_t index) {
+    Boilerplate* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void boilerplate_scene_menu_on_enter(void* context) {
+    Boilerplate* app = context;
+
+    submenu_add_item(
+        app->submenu,
+        "Photo Viewer",
+        SubmenuIndexScene1,
+        boilerplate_scene_menu_submenu_callback,
+        app);
+    submenu_add_item(
+        app->submenu,
+        "Export All",
+        SubmenuIndexScene2,
+        boilerplate_scene_menu_submenu_callback,
+        app);
+    // submenu_add_item(app->submenu, "Scene 3 (Buttonmenu)", SubmenuIndexScene3, boilerplate_scene_menu_submenu_callback, app);
+    // submenu_add_item(app->submenu, "Scene 4 (File Browser)", SubmenuIndexScene4, boilerplate_scene_menu_submenu_callback, app);
+    submenu_add_item(
+        app->submenu,
+        "Settings",
+        SubmenuIndexSettings,
+        boilerplate_scene_menu_submenu_callback,
+        app);
+
+    submenu_set_selected_item(
+        app->submenu, scene_manager_get_scene_state(app->scene_manager, BoilerplateSceneMenu));
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, BoilerplateViewIdMenu);
+}
+
+bool boilerplate_scene_menu_on_event(void* context, SceneManagerEvent event) {
+    Boilerplate* app = context;
+    UNUSED(app);
+    if(event.type == SceneManagerEventTypeBack) {
+        //exit app
+        scene_manager_stop(app->scene_manager);
+        view_dispatcher_stop(app->view_dispatcher);
+        return true;
+    } else if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexScene1) {
+            scene_manager_set_scene_state(
+                app->scene_manager, BoilerplateSceneMenu, SubmenuIndexScene1);
+            scene_manager_next_scene(app->scene_manager, BoilerplateSceneScene_1);
+            return true;
+        } else if(event.event == SubmenuIndexScene2) {
+            scene_manager_set_scene_state(
+                app->scene_manager, BoilerplateSceneMenu, SubmenuIndexScene2);
+            scene_manager_next_scene(app->scene_manager, BoilerplateSceneScene_2);
+            return true;
+            // } else if (event.event == SubmenuIndexScene3) {
+            //     scene_manager_set_scene_state(
+            //         app->scene_manager, BoilerplateSceneMenu, SubmenuIndexScene3);
+            //     scene_manager_next_scene(app->scene_manager, BoilerplateSceneScene_3);
+            // } else if (event.event == SubmenuIndexScene4) {
+            //     scene_manager_set_scene_state(
+            //         app->scene_manager, BoilerplateSceneMenu, SubmenuIndexScene4);
+            //     scene_manager_next_scene(app->scene_manager, BoilerplateSceneScene_4);
+        } else if(event.event == SubmenuIndexSettings) {
+            scene_manager_set_scene_state(
+                app->scene_manager, BoilerplateSceneMenu, SubmenuIndexSettings);
+            scene_manager_next_scene(app->scene_manager, BoilerplateSceneSettings);
+            return true;
+        }
+    }
+    return false;
+}
+
+void boilerplate_scene_menu_on_exit(void* context) {
+    Boilerplate* app = context;
+    submenu_reset(app->submenu);
+}

+ 50 - 0
malveke_gb_photo/scenes/boilerplate_scene_scene_1.c

@@ -0,0 +1,50 @@
+#include "../malveke_gb_photo.h"
+#include "../helpers/boilerplate_custom_event.h"
+#include "../views/boilerplate_scene_1.h"
+
+void boilerplate_scene_1_callback(BoilerplateCustomEvent event, void* context) {
+    furi_assert(context);
+    Boilerplate* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void boilerplate_scene_scene_1_on_enter(void* context) {
+    furi_assert(context);
+    Boilerplate* app = context;
+    boilerplate_scene_1_set_callback(app->boilerplate_scene_1, boilerplate_scene_1_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, BoilerplateViewIdScene1);
+}
+
+bool boilerplate_scene_scene_1_on_event(void* context, SceneManagerEvent event) {
+    Boilerplate* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case BoilerplateCustomEventScene1Left:
+        case BoilerplateCustomEventScene1Right:
+            break;
+        case BoilerplateCustomEventScene1Up:
+        case BoilerplateCustomEventScene1Down:
+            break;
+        case BoilerplateCustomEventScene1Back:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, BoilerplateSceneMenu)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void boilerplate_scene_scene_1_on_exit(void* context) {
+    Boilerplate* app = context;
+    UNUSED(app);
+}

+ 50 - 0
malveke_gb_photo/scenes/boilerplate_scene_scene_2.c

@@ -0,0 +1,50 @@
+#include "../malveke_gb_photo.h"
+#include "../helpers/boilerplate_custom_event.h"
+#include "../views/boilerplate_scene_2.h"
+
+void boilerplate_scene_2_callback(BoilerplateCustomEvent event, void* context) {
+    furi_assert(context);
+    Boilerplate* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void boilerplate_scene_scene_2_on_enter(void* context) {
+    furi_assert(context);
+    Boilerplate* app = context;
+    boilerplate_scene_2_set_callback(app->boilerplate_scene_2, boilerplate_scene_2_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, BoilerplateViewIdScene2);
+}
+
+bool boilerplate_scene_scene_2_on_event(void* context, SceneManagerEvent event) {
+    Boilerplate* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case BoilerplateCustomEventScene2Left:
+        case BoilerplateCustomEventScene2Right:
+            break;
+        case BoilerplateCustomEventScene2Up:
+        case BoilerplateCustomEventScene2Down:
+            break;
+        case BoilerplateCustomEventScene2Back:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, BoilerplateSceneMenu)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void boilerplate_scene_scene_2_on_exit(void* context) {
+    Boilerplate* app = context;
+    UNUSED(app);
+}

+ 515 - 0
malveke_gb_photo/scenes/boilerplate_scene_settings.c

@@ -0,0 +1,515 @@
+#include "../malveke_gb_photo.h"
+#include <lib/toolbox/value_index.h>
+
+enum SettingsIndex {
+    SettingsIndexHaptic = 10,
+    SettingsIndexValue1,
+    SettingsIndexValue2,
+};
+
+const char* const palette_text[57] = {
+    "B&W",
+    "Original",
+    "Splash Up",
+    "GB Light",
+    "Pocket",
+    "aqpp",
+    "azc",
+    "banana",
+    "bgb",
+    "blackzero",
+    "cctr",
+    "cfp",
+    "cga1",
+    "cga2",
+    "chig",
+    "cmyk",
+    "cybl",
+    "d2kr",
+    "datn",
+    "dhg",
+    "dimwm",
+    "ffs",
+    "fsil",
+    "gbcd",
+    "gbcda",
+    "gbcdb",
+    "gbceuus",
+    "gbcl",
+    "gbcla",
+    "gbclb",
+    "gbcr",
+    "gbcrb",
+    "gbcua",
+    "gbcub",
+    "gelc",
+    "glmo",
+    "grafixkidgray",
+    "grafixkidgreen",
+    "hipster",
+    "kditw",
+    "llawk",
+    "marmx",
+    "nc",
+    "ppr",
+    "rcs",
+    "roga",
+    "sfh",
+    "shmgy",
+    "shzol",
+    "slmem",
+    "spezi",
+    "tdoyc",
+    "tpa",
+    "tsk",
+    "vb85",
+    "wtfp",
+    "yirl"};
+const uint32_t palette_value[57] = {
+    BoilerplatePaletteBlackAndWhite,
+    BoilerplatePaletteOriginal,
+    BoilerplatePaletteSplashUp,
+    BoilerplatePaletteGBLight,
+    BoilerplatePalettePocket,
+    BoilerplatePaletteAudiQuattroPikesPeak,
+    BoilerplatePaletteAzureClouds,
+    BoilerplatePaletteTheresalwaysmoney,
+    BoilerplatePaletteBGBEmulator,
+    BoilerplatePaletteGameBoyBlackZeropalette,
+    BoilerplatePaletteCandyCottonTowerRaid,
+    BoilerplatePaletteCaramelFudgeParanoia,
+    BoilerplatePaletteCGAPaletteCrush1,
+    BoilerplatePaletteCGAPaletteCrush2,
+    BoilerplatePaletteChildhoodinGreenland,
+    BoilerplatePaletteCMYKeystone,
+    BoilerplatePaletteCyanideBlues,
+    BoilerplatePaletteDune2000remastered,
+    BoilerplatePaletteDrowningatnight,
+    BoilerplatePaletteDeepHazeGreen,
+    BoilerplatePaletteDiesistmeineWassermelone,
+    BoilerplatePaletteFlowerfeldstrabe,
+    BoilerplatePaletteFloydSteinberginLove,
+    BoilerplatePaletteGameBoyColorSplashDown,
+    BoilerplatePaletteGameBoyColorSplashDownA,
+    BoilerplatePaletteGameBoyColorSplashDownB,
+    BoilerplatePaletteGameBoyColorSplashRightAGameBoyCamera,
+    BoilerplatePaletteGameBoyColorSplashLeft,
+    BoilerplatePaletteGameBoyColorSplashLeftA,
+    BoilerplatePaletteGameBoyColorSplashLeftB,
+    BoilerplatePaletteGameBoyColorSplashRight,
+    BoilerplatePaletteGameBoyColorSplashRightB,
+    BoilerplatePaletteGameBoyColorSplashUpA,
+    BoilerplatePaletteGameBoyColorSplashUpB,
+    BoilerplatePaletteGoldenElephantCurry,
+    BoilerplatePaletteGlowingMountains,
+    BoilerplatePaletteGrafixkidGray,
+    BoilerplatePaletteGrafixkidGreen,
+    BoilerplatePaletteArtisticCaffeinatedLactose,
+    BoilerplatePaletteKneeDeepintheWood,
+    BoilerplatePaletteLinkslateAwakening,
+    BoilerplatePaletteMetroidAranremixed,
+    BoilerplatePaletteNortoriousComandante,
+    BoilerplatePalettePurpleRain,
+    BoilerplatePaletteRustedCitySign,
+    BoilerplatePaletteRomerosGarden,
+    BoilerplatePaletteSunflowerHolidays,
+    BoilerplatePaletteSuperHyperMegaGameboy,
+    BoilerplatePaletteSpaceHazeOverload,
+    BoilerplatePaletteStarlitMemories,
+    BoilerplatePaletteMyFriendfromBavaria,
+    BoilerplatePaletteThedeathofYungColumbus,
+    BoilerplatePaletteTramontoalParcodegliAcquedotti,
+    BoilerplatePaletteThestarryknight,
+    BoilerplatePaletteVirtualBoy1985,
+    BoilerplatePaletteWaterfrontPlaza,
+    BoilerplatePaletteYouthIkarusreloaded};
+
+const char* const info_text[2] = {
+    "OFF",
+    "ON",
+};
+const uint32_t info_value[2] = {
+    BoilerplateInfoOff,
+    BoilerplateInfoOn,
+};
+
+static void boilerplate_scene_settings_set_palette(VariableItem* item) {
+    Boilerplate* app = variable_item_get_context(item);
+    UNUSED(app);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, palette_text[index]);
+    app->palette = palette_value[index];
+    if(app->palette == BoilerplatePaletteBlackAndWhite) {
+        app->palette_color_hex_a = 0xFFFFFF;
+        app->palette_color_hex_b = 0xAAAAAA;
+        app->palette_color_hex_c = 0x555555;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteOriginal) {
+        app->palette_color_hex_a = 0x9bbc0f;
+        app->palette_color_hex_b = 0x77a112;
+        app->palette_color_hex_c = 0x306230;
+        app->palette_color_hex_d = 0x0f380f;
+    } else if(app->palette == BoilerplatePaletteSplashUp) {
+        app->palette_color_hex_a = 0xffffff;
+        app->palette_color_hex_b = 0xffad63;
+        app->palette_color_hex_c = 0x833100;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteGBLight) {
+        app->palette_color_hex_a = 0x1ddece;
+        app->palette_color_hex_b = 0x19c7b3;
+        app->palette_color_hex_c = 0x16a596;
+        app->palette_color_hex_d = 0x0b7a6d;
+    } else if(app->palette == BoilerplatePalettePocket) {
+        app->palette_color_hex_a = 0xc4cfa1;
+        app->palette_color_hex_b = 0x8b956d;
+        app->palette_color_hex_c = 0x4d533c;
+        app->palette_color_hex_d = 0x1f1f1f;
+    } else if(app->palette == BoilerplatePaletteAudiQuattroPikesPeak) {
+        app->palette_color_hex_a = 0xebeee7;
+        app->palette_color_hex_b = 0x868779;
+        app->palette_color_hex_c = 0xfa2b25;
+        app->palette_color_hex_d = 0x2a201e;
+    } else if(app->palette == BoilerplatePaletteAzureClouds) {
+        app->palette_color_hex_a = 0x47ff99;
+        app->palette_color_hex_b = 0x32b66d;
+        app->palette_color_hex_c = 0x124127;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteTheresalwaysmoney) {
+        app->palette_color_hex_a = 0xfdfe0a;
+        app->palette_color_hex_b = 0xfed638;
+        app->palette_color_hex_c = 0x977b25;
+        app->palette_color_hex_d = 0x221a09;
+    } else if(app->palette == BoilerplatePaletteBGBEmulator) {
+        app->palette_color_hex_a = 0xe0f8d0;
+        app->palette_color_hex_b = 0x88c070;
+        app->palette_color_hex_c = 0x346856;
+        app->palette_color_hex_d = 0x081820;
+    } else if(app->palette == BoilerplatePaletteGameBoyBlackZeropalette) {
+        app->palette_color_hex_a = 0x7e8416;
+        app->palette_color_hex_b = 0x577b46;
+        app->palette_color_hex_c = 0x385d49;
+        app->palette_color_hex_d = 0x2e463d;
+    } else if(app->palette == BoilerplatePaletteCandyCottonTowerRaid) {
+        app->palette_color_hex_a = 0xe6aec4;
+        app->palette_color_hex_b = 0xe65790;
+        app->palette_color_hex_c = 0x8f0039;
+        app->palette_color_hex_d = 0x380016;
+    } else if(app->palette == BoilerplatePaletteCaramelFudgeParanoia) {
+        app->palette_color_hex_a = 0xcf9255;
+        app->palette_color_hex_b = 0xcf7163;
+        app->palette_color_hex_c = 0xb01553;
+        app->palette_color_hex_d = 0x3f1711;
+    } else if(app->palette == BoilerplatePaletteCGAPaletteCrush1) {
+        app->palette_color_hex_a = 0xffffff;
+        app->palette_color_hex_b = 0x55ffff;
+        app->palette_color_hex_c = 0xff55ff;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteCGAPaletteCrush2) {
+        app->palette_color_hex_a = 0xffffff;
+        app->palette_color_hex_b = 0x55ffff;
+        app->palette_color_hex_c = 0xff5555;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteChildhoodinGreenland) {
+        app->palette_color_hex_a = 0xd0d058;
+        app->palette_color_hex_b = 0xa0a840;
+        app->palette_color_hex_c = 0x708028;
+        app->palette_color_hex_d = 0x405010;
+    } else if(app->palette == BoilerplatePaletteCMYKeystone) {
+        app->palette_color_hex_a = 0xffff00;
+        app->palette_color_hex_b = 0x0be8fd;
+        app->palette_color_hex_c = 0xfb00fa;
+        app->palette_color_hex_d = 0x373737;
+    } else if(app->palette == BoilerplatePaletteCyanideBlues) {
+        app->palette_color_hex_a = 0x9efbe3;
+        app->palette_color_hex_b = 0x21aff5;
+        app->palette_color_hex_c = 0x1e4793;
+        app->palette_color_hex_d = 0x0e1e3d;
+    } else if(app->palette == BoilerplatePaletteDune2000remastered) {
+        app->palette_color_hex_a = 0xfbf1cd;
+        app->palette_color_hex_b = 0xc09e7d;
+        app->palette_color_hex_c = 0x725441;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteDrowningatnight) {
+        app->palette_color_hex_a = 0xa9b0b3;
+        app->palette_color_hex_b = 0x586164;
+        app->palette_color_hex_c = 0x20293f;
+        app->palette_color_hex_d = 0x030c22;
+    } else if(app->palette == BoilerplatePaletteDeepHazeGreen) {
+        app->palette_color_hex_a = 0xa1d909;
+        app->palette_color_hex_b = 0x467818;
+        app->palette_color_hex_c = 0x27421f;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteDiesistmeineWassermelone) {
+        app->palette_color_hex_a = 0xffdbcb;
+        app->palette_color_hex_b = 0xf27d7a;
+        app->palette_color_hex_c = 0x558429;
+        app->palette_color_hex_d = 0x222903;
+    } else if(app->palette == BoilerplatePaletteFlowerfeldstrabe) {
+        app->palette_color_hex_a = 0xe9d9cc;
+        app->palette_color_hex_b = 0xc5c5ce;
+        app->palette_color_hex_c = 0x75868f;
+        app->palette_color_hex_d = 0x171f62;
+    } else if(app->palette == BoilerplatePaletteFloydSteinberginLove) {
+        app->palette_color_hex_a = 0xeaf5fa;
+        app->palette_color_hex_b = 0x5fb1f5;
+        app->palette_color_hex_c = 0xd23c4e;
+        app->palette_color_hex_d = 0x4c1c2d;
+    } else if(app->palette == BoilerplatePaletteGameBoyColorSplashDown) {
+        app->palette_color_hex_a = 0xffffa5;
+        app->palette_color_hex_b = 0xfe9494;
+        app->palette_color_hex_c = 0x9394fe;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteGameBoyColorSplashDownA) {
+        app->palette_color_hex_a = 0xffffff;
+        app->palette_color_hex_b = 0xffff00;
+        app->palette_color_hex_c = 0xfe0000;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteGameBoyColorSplashDownB) {
+        app->palette_color_hex_a = 0xffffff;
+        app->palette_color_hex_b = 0xffff00;
+        app->palette_color_hex_c = 0x7d4900;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteGameBoyColorSplashRightAGameBoyCamera) {
+        app->palette_color_hex_a = 0xffffff;
+        app->palette_color_hex_b = 0x7bff30;
+        app->palette_color_hex_c = 0x0163c6;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteGameBoyColorSplashLeft) {
+        app->palette_color_hex_a = 0xffffff;
+        app->palette_color_hex_b = 0x65a49b;
+        app->palette_color_hex_c = 0x0000fe;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteGameBoyColorSplashLeftA) {
+        app->palette_color_hex_a = 0xffffff;
+        app->palette_color_hex_b = 0x8b8cde;
+        app->palette_color_hex_c = 0x53528c;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteGameBoyColorSplashLeftB) {
+        app->palette_color_hex_a = 0xffffff;
+        app->palette_color_hex_b = 0xa5a5a5;
+        app->palette_color_hex_c = 0x525252;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteGameBoyColorSplashRight) {
+        app->palette_color_hex_a = 0xffffff;
+        app->palette_color_hex_b = 0x51ff00;
+        app->palette_color_hex_c = 0xff4200;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteGameBoyColorSplashRightB) {
+        app->palette_color_hex_a = 0x000000;
+        app->palette_color_hex_b = 0x008486;
+        app->palette_color_hex_c = 0xffde00;
+        app->palette_color_hex_d = 0xffffff;
+    } else if(app->palette == BoilerplatePaletteGameBoyColorSplashUpA) {
+        app->palette_color_hex_a = 0xffffff;
+        app->palette_color_hex_b = 0xff8f84;
+        app->palette_color_hex_c = 0x943a3a;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteGameBoyColorSplashUpB) {
+        app->palette_color_hex_a = 0xffe7c5;
+        app->palette_color_hex_b = 0xce9c85;
+        app->palette_color_hex_c = 0x846b29;
+        app->palette_color_hex_d = 0x5b3109;
+    } else if(app->palette == BoilerplatePaletteGoldenElephantCurry) {
+        app->palette_color_hex_a = 0xff9c00;
+        app->palette_color_hex_b = 0xc27600;
+        app->palette_color_hex_c = 0x4f3000;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteGlowingMountains) {
+        app->palette_color_hex_a = 0xffbf98;
+        app->palette_color_hex_b = 0xa1a8b8;
+        app->palette_color_hex_c = 0x514f6c;
+        app->palette_color_hex_d = 0x2f1c35;
+    } else if(app->palette == BoilerplatePaletteGrafixkidGray) {
+        app->palette_color_hex_a = 0xe0dbcd;
+        app->palette_color_hex_b = 0xa89f94;
+        app->palette_color_hex_c = 0x706b66;
+        app->palette_color_hex_d = 0x2b2b26;
+    } else if(app->palette == BoilerplatePaletteGrafixkidGreen) {
+        app->palette_color_hex_a = 0xdbf4b4;
+        app->palette_color_hex_b = 0xabc396;
+        app->palette_color_hex_c = 0x7b9278;
+        app->palette_color_hex_d = 0x4c625a;
+    } else if(app->palette == BoilerplatePaletteArtisticCaffeinatedLactose) {
+        app->palette_color_hex_a = 0xfdfef5;
+        app->palette_color_hex_b = 0xdea963;
+        app->palette_color_hex_c = 0x9e754f;
+        app->palette_color_hex_d = 0x241606;
+    } else if(app->palette == BoilerplatePaletteKneeDeepintheWood) {
+        app->palette_color_hex_a = 0xfffe6e;
+        app->palette_color_hex_b = 0xd5690f;
+        app->palette_color_hex_c = 0x3c3ca9;
+        app->palette_color_hex_d = 0x2c2410;
+    } else if(app->palette == BoilerplatePaletteLinkslateAwakening) {
+        app->palette_color_hex_a = 0xffffb5;
+        app->palette_color_hex_b = 0x7bc67b;
+        app->palette_color_hex_c = 0x6b8c42;
+        app->palette_color_hex_d = 0x5a3921;
+    } else if(app->palette == BoilerplatePaletteMetroidAranremixed) {
+        app->palette_color_hex_a = 0xaedf1e;
+        app->palette_color_hex_b = 0x047e60;
+        app->palette_color_hex_c = 0xb62558;
+        app->palette_color_hex_d = 0x2c1700;
+    } else if(app->palette == BoilerplatePaletteNortoriousComandante) {
+        app->palette_color_hex_a = 0xfcfe54;
+        app->palette_color_hex_b = 0x54fefc;
+        app->palette_color_hex_c = 0x04aaac;
+        app->palette_color_hex_d = 0x0402ac;
+    } else if(app->palette == BoilerplatePalettePurpleRain) {
+        app->palette_color_hex_a = 0xadfffc;
+        app->palette_color_hex_b = 0x8570b2;
+        app->palette_color_hex_c = 0xff0084;
+        app->palette_color_hex_d = 0x68006a;
+    } else if(app->palette == BoilerplatePaletteRustedCitySign) {
+        app->palette_color_hex_a = 0xedb4a1;
+        app->palette_color_hex_b = 0xa96868;
+        app->palette_color_hex_c = 0x764462;
+        app->palette_color_hex_d = 0x2c2137;
+    } else if(app->palette == BoilerplatePaletteRomerosGarden) {
+        app->palette_color_hex_a = 0xebc4ab;
+        app->palette_color_hex_b = 0x649a57;
+        app->palette_color_hex_c = 0x574431;
+        app->palette_color_hex_d = 0x323727;
+    } else if(app->palette == BoilerplatePaletteSunflowerHolidays) {
+        app->palette_color_hex_a = 0xffff55;
+        app->palette_color_hex_b = 0xff5555;
+        app->palette_color_hex_c = 0x881400;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteSuperHyperMegaGameboy) {
+        app->palette_color_hex_a = 0xf7e7c6;
+        app->palette_color_hex_b = 0xd68e49;
+        app->palette_color_hex_c = 0xa63725;
+        app->palette_color_hex_d = 0x331e50;
+    } else if(app->palette == BoilerplatePaletteSpaceHazeOverload) {
+        app->palette_color_hex_a = 0xf8e3c4;
+        app->palette_color_hex_b = 0xcc3495;
+        app->palette_color_hex_c = 0x6b1fb1;
+        app->palette_color_hex_d = 0x0b0630;
+    } else if(app->palette == BoilerplatePaletteStarlitMemories) {
+        app->palette_color_hex_a = 0x869ad9;
+        app->palette_color_hex_b = 0x6d53bd;
+        app->palette_color_hex_c = 0x6f2096;
+        app->palette_color_hex_d = 0x4f133f;
+    } else if(app->palette == BoilerplatePaletteMyFriendfromBavaria) {
+        app->palette_color_hex_a = 0xfeda1b;
+        app->palette_color_hex_b = 0xdf7925;
+        app->palette_color_hex_c = 0xb60077;
+        app->palette_color_hex_d = 0x382977;
+    } else if(app->palette == BoilerplatePaletteThedeathofYungColumbus) {
+        app->palette_color_hex_a = 0xb5ff32;
+        app->palette_color_hex_b = 0xff2261;
+        app->palette_color_hex_c = 0x462917;
+        app->palette_color_hex_d = 0x1d1414;
+    } else if(app->palette == BoilerplatePaletteTramontoalParcodegliAcquedotti) {
+        app->palette_color_hex_a = 0xf3c677;
+        app->palette_color_hex_b = 0xe64a4e;
+        app->palette_color_hex_c = 0x912978;
+        app->palette_color_hex_d = 0x0c0a3e;
+    } else if(app->palette == BoilerplatePaletteThestarryknight) {
+        app->palette_color_hex_a = 0xf5db37;
+        app->palette_color_hex_b = 0x37cae5;
+        app->palette_color_hex_c = 0x0f86b6;
+        app->palette_color_hex_d = 0x123f77;
+    } else if(app->palette == BoilerplatePaletteVirtualBoy1985) {
+        app->palette_color_hex_a = 0xff0000;
+        app->palette_color_hex_b = 0xdb0000;
+        app->palette_color_hex_c = 0x520000;
+        app->palette_color_hex_d = 0x000000;
+    } else if(app->palette == BoilerplatePaletteWaterfrontPlaza) {
+        app->palette_color_hex_a = 0xcecece;
+        app->palette_color_hex_b = 0x6f9edf;
+        app->palette_color_hex_c = 0x42678e;
+        app->palette_color_hex_d = 0x102533;
+    } else if(app->palette == BoilerplatePaletteYouthIkarusreloaded) {
+        app->palette_color_hex_a = 0xcef7f7;
+        app->palette_color_hex_b = 0xf78e50;
+        app->palette_color_hex_c = 0x9e0000;
+        app->palette_color_hex_d = 0x1e0000;
+    }
+}
+
+static void boilerplate_scene_settings_set_info(VariableItem* item) {
+    Boilerplate* app = variable_item_get_context(item);
+    UNUSED(app);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, info_text[index]);
+    app->info = info_value[index];
+}
+
+// static void boilerplate_scene_settings_set_led(VariableItem* item) {
+//     Boilerplate* app = variable_item_get_context(item);
+//     uint8_t index = variable_item_get_current_value_index(item);
+//     variable_item_set_current_value_text(item, led_text[index]);
+//     app->led = led_value[index];
+// }
+
+// static void boilerplate_scene_settings_set_save_settings(VariableItem* item) {
+//     Boilerplate* app = variable_item_get_context(item);
+//     uint8_t index = variable_item_get_current_value_index(item);
+//     variable_item_set_current_value_text(item, settings_text[index]);
+//     app->save_settings = settings_value[index];
+// }
+
+void boilerplate_scene_settings_submenu_callback(void* context, uint32_t index) {
+    Boilerplate* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void boilerplate_scene_settings_on_enter(void* context) {
+    Boilerplate* app = context;
+    VariableItem* item;
+    uint8_t value_index;
+
+    // Palette
+    item = variable_item_list_add(
+        app->variable_item_list, "Palette:", 57, boilerplate_scene_settings_set_palette, app);
+    value_index = value_index_uint32(app->palette, palette_value, 1);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, palette_text[value_index]);
+
+    // Info
+    item = variable_item_list_add(
+        app->variable_item_list, "Info:", 2, boilerplate_scene_settings_set_info, app);
+    value_index = value_index_uint32(app->info, info_value, 2);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, info_text[value_index]);
+
+    // // LED Effects on/off
+    // item = variable_item_list_add(
+    //     app->variable_item_list,
+    //     "Save Prefix",
+    //     2,
+    //     boilerplate_scene_settings_set_led,
+    //     app);
+    // value_index = value_index_uint32(app->led, led_value, 2);
+    // variable_item_set_current_value_index(item, value_index);
+    // variable_item_set_current_value_text(item, led_text[value_index]);
+
+    // // Save Settings to File
+    // item = variable_item_list_add(
+    //     app->variable_item_list,
+    //     "Save Settings",
+    //     2,
+    //     boilerplate_scene_settings_set_save_settings,
+    //     app);
+    // value_index = value_index_uint32(app->save_settings, settings_value, 2);
+    // variable_item_set_current_value_index(item, value_index);
+    // variable_item_set_current_value_text(item, settings_text[value_index]);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, BoilerplateViewIdSettings);
+}
+
+bool boilerplate_scene_settings_on_event(void* context, SceneManagerEvent event) {
+    Boilerplate* app = context;
+    UNUSED(app);
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+    }
+    return consumed;
+}
+
+void boilerplate_scene_settings_on_exit(void* context) {
+    Boilerplate* app = context;
+    variable_item_list_set_selected_item(app->variable_item_list, 0);
+    variable_item_list_reset(app->variable_item_list);
+}

+ 55 - 0
malveke_gb_photo/scenes/boilerplate_scene_startscreen.c

@@ -0,0 +1,55 @@
+#include "../malveke_gb_photo.h"
+#include "../helpers/boilerplate_custom_event.h"
+#include "../views/boilerplate_startscreen.h"
+
+void boilerplate_scene_startscreen_callback(BoilerplateCustomEvent event, void* context) {
+    furi_assert(context);
+    Boilerplate* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, event);
+}
+
+void boilerplate_scene_startscreen_on_enter(void* context) {
+    furi_assert(context);
+    Boilerplate* app = context;
+    boilerplate_startscreen_set_callback(
+        app->boilerplate_startscreen, boilerplate_scene_startscreen_callback, app);
+    view_dispatcher_switch_to_view(app->view_dispatcher, BoilerplateViewIdStartscreen);
+}
+
+bool boilerplate_scene_startscreen_on_event(void* context, SceneManagerEvent event) {
+    Boilerplate* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case BoilerplateCustomEventStartscreenLeft:
+        case BoilerplateCustomEventStartscreenRight:
+            break;
+        case BoilerplateCustomEventStartscreenUp:
+        case BoilerplateCustomEventStartscreenDown:
+            break;
+        case BoilerplateCustomEventStartscreenOk:
+            scene_manager_next_scene(app->scene_manager, BoilerplateSceneFileselect);
+            consumed = true;
+            break;
+        case BoilerplateCustomEventStartscreenBack:
+            notification_message(app->notification, &sequence_reset_red);
+            notification_message(app->notification, &sequence_reset_green);
+            notification_message(app->notification, &sequence_reset_blue);
+            if(!scene_manager_search_and_switch_to_previous_scene(
+                   app->scene_manager, BoilerplateSceneStartscreen)) {
+                scene_manager_stop(app->scene_manager);
+                view_dispatcher_stop(app->view_dispatcher);
+            }
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void boilerplate_scene_startscreen_on_exit(void* context) {
+    Boilerplate* app = context;
+    UNUSED(app);
+}

+ 54 - 0
malveke_gb_photo/scenes/scene_file_select.c

@@ -0,0 +1,54 @@
+#include "../malveke_gb_photo.h"
+#include <storage/storage.h>
+
+static bool file_select(Boilerplate* app) {
+    furi_assert(app);
+
+    furi_string_set(app->file_path, MALVEKE_APP_FOLDER_RAMS);
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(
+        &browser_options, MALVEKE_APP_RAM_EXTENSION, &I_icon_10px);
+    browser_options.base_path = MALVEKE_APP_FOLDER_RAMS;
+    browser_options.skip_assets = true;
+
+    // Input events and views are managed by file_browser
+    bool res =
+        dialog_file_browser_show(app->dialogs, app->file_path, app->file_path, &browser_options);
+    return res;
+}
+
+void boilerplate_scene_fileselect_on_enter(void* context) {
+    Boilerplate* app = context;
+
+    // if(app->script) {
+    //     script_close(app->script);
+    //     app->script = NULL;
+    // }
+    // if(app->camera_ram_sav && storage_file_is_open(app->camera_ram_sav)) {
+    //     storage_file_close(app->camera_ram_sav);
+    // }
+    // app->camera_ram_sav = storage_file_alloc(app->storage);
+    if(file_select(app)) {
+        if(storage_file_open(
+               app->camera_ram_sav,
+               furi_string_get_cstr(app->file_path),
+               FSAM_READ,
+               FSOM_OPEN_EXISTING)) {
+            scene_manager_next_scene(app->scene_manager, BoilerplateSceneMenu);
+        }
+    } else {
+        view_dispatcher_stop(app->view_dispatcher);
+    }
+}
+
+bool boilerplate_scene_fileselect_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    // Boilerplate* app = context;
+    return false;
+}
+
+void boilerplate_scene_fileselect_on_exit(void* context) {
+    UNUSED(context);
+    // Boilerplate* app = context;
+}

+ 77 - 0
malveke_gb_photo/u8g2_font_5x7_mf.h

@@ -0,0 +1,77 @@
+#pragma once
+
+#ifndef U8G2_FONT_5X7_H
+#define U8G2_FONT_5X7_H
+
+#include <stdint.h>
+
+/*
+  Fontname: -Misc-Fixed-Medium-R-Normal--7-70-75-75-C-50-ISO10646-1
+  Copyright: Public domain font.  Share and enjoy.
+  Glyphs: 191/1848
+  BBX Build Mode: 2
+*/
+
+const uint8_t u8g2_font_5x7_mf[1911] =
+    "\277\2\3\2\3\3\1\1\4\5\7\0\377\6\377\6\0\1*\2r\7Z \5}}\36!\10}\255"
+    "\260\35\312\1\42\11}\235R\22%\71\3#\13}\355$\31\224\312\240\224\1$\11}\355\245\266%\311"
+    "\14%\11}\215\60\312\232\302\30&\12}\355,\311*Y\22\3'\7}\255\260\316\2(\10}\255,"
+    "l\315\1)\11}\235\64l\313\21\0*\11}\355$\313\266J\14+\11}\375\60\32\244\60\7,\7"
+    "}}\26\255\6-\10}}\312\220\223\0.\7}}\272\246\3/\7}}$k'\60\14}\255,"
+    "\211\222(\211\222,\7\61\10}\255Ll\233\1\62\11}\35\251\230\325\206\30\63\13}\215!\214\324$"
+    "\212t\0\64\13}\255LJ\242!\13s\0\65\13}\215!\11\327$\212t\0\66\12}\35)\234*"
+    "Q\244\3\67\13}\215!\314\302,\314\21\0\70\12}\35\251$U\242H\7\71\12}\35\251\22Ec"
+    "\244\3:\10}mMG\64\35;\11}mMG\264\42\0<\10}}$\253\226\1=\11}}l"
+    "\210\207\34\4>\10}\355\264VG\0\77\12}\255,\11\263\34\312\1@\13}\35\251\222(\211\222\352"
+    "\0A\14}\35\251\22%C\22%Q\14B\12}\215\251\62U\242d\7C\11}\35\251\22\226\42\35"
+    "D\14}\215\251\22%Q\22%;\0E\13}\215!\11\247\60\34b\0F\12}\215!\11\247\260\16"
+    "\1G\13}\35\251\22&J\24\315\0H\16}\215(\211\222!\211\222(\211b\0I\10}\235-l"
+    "\233\1J\11}\275\260%\212t\0K\13}\215()iZ\22\225\1L\10}\215\260\343\20\3M\16"
+    "}\215(\31\222!\211\222(\211b\0N\15}\215(Q\22\245\242$J\24\3O\14}\35\251\22%"
+    "Q\22E:\0P\13}\215\251\22%S\230C\0Q\14}\35\251\22%Q\242Dj\2R\13}\215"
+    "\251\22%S\22\225\1S\11}\35\251\65*\351\0T\7}\235-\354\16U\15}\215(\211\222(\211"
+    "\222(\322\1V\14}\215(\211\222(\211\42M\7W\16}\215(\211\222(\31\222!\211b\0X\14"
+    "}\215(\211\42M\252D\61\0Y\12}\235R\22%YX\7Z\11}\215!\314\32\207\30[\10}"
+    "\235)l\234\1\134\7}\335\356 \0]\7}\235\261\323\14^\10}\255,\311Y\1_\7}}\306"
+    "!\6`\7}\235\64g\6a\13}}p\211\222DJb\0b\12}\215\60\234*Q\262\3c\10"
+    "}}P\12S\35d\13}\275\60Z\242$\212f\0e\11}}PJ\214:\0f\13}\255,\211"
+    "\262-\314\21\0g\12}}p\211\42)]\0h\13}\215\60\234*Q\22\305\0i\11}\255\34\21"
+    "k\63\0j\12}\275\34\12KI\26\1k\12}\215\260\22iIT\6l\7}\35\261\333\14m\13"
+    "}},\211\206$J\242\30n\12}}l\252DI\24\3o\11}}P\252D\221\16p\12}}"
+    "l\252D\311\24\2q\13}}p\211\222(\32\23\0r\11}}l\252\204\71\4s\10}}pQ"
+    "\225\35t\11}\235\60\333\302T\6u\13}},J\242$\212f\0v\13}}\60\211\222(\311r"
+    "\0w\13}},J\242dH\206\30x\11}},\212\64\251\14y\12}},J\242JV\3z"
+    "\11}}l\310jC\14{\10}\275\232X\215\1|\7}\255\260w\0}\12}\235\64\324\302,G"
+    "\0~\7}\235\356\314\0\240\5}}\36\241\10}\255\34\12\333\1\242\12}\375l)%\331\26\1\243"
+    "\11}})\333*\62\0\244\13}\335,\231\222h\311R\0\245\12}\235R\222e[\230\3\246\10}"
+    "\375\60\207\302\34\247\13}-)\324\222L\214$\0\250\7}\235:\67\0\251\14}\235%S\222II"
+    "\264d\1\252\11}\35)\311t\26\0\253\11}}\60\222\332\61\0\254\11}}\312\20\346 \0\255\7"
+    "}}\322N\2\256\13}\235%\33\222i\322\222\5\257\7}\215!\347\6\260\11}\255,\311r\26\0"
+    "\261\13}\255\60\32\244\60\32T\0\262\11}\35\61\13u\32\0\263\11}\35M\314t\32\0\264\7}"
+    "\255,\347\0\265\13}},J\242$J\246\20\266\15}\235EI\224(\211\222(\211\1\267\10}}"
+    "P\323i\0\270\7}}\346,\3\271\11}\255L\314v\22\0\272\11}\235,\311r&\0\273\11}"
+    "},*I\71\10\274\11}\215\260)\223\306\4\275\11}\215\260E\314B\5\276\13}\15M\314\224L"
+    "\32\23\0\277\12}\255\34\312\302$\313\1\300\14}\35\251\22%C\22%Q\14\301\14}\35\251\22%"
+    "C\22%Q\14\302\14}\35\251\22%C\22%Q\14\303\14}\35\251\22%C\22%Q\14\304\14}"
+    "\215(\222*C\22%Q\14\305\14}\35M\252\14I\224D\61\0\306\14}\235\245\224(S\22%\62"
+    "\0\307\12}\35\251\22\226\42-\3\310\13}\215!\11\247\60\34b\0\311\13}\215!\11\247\60\34b"
+    "\0\312\13}\215!\11\247\60\34b\0\313\13}\215!\11\247\60\34b\0\314\10}\235-l\233\1\315"
+    "\10}\235-l\233\1\316\10}\235-l\233\1\317\10}\235-l\233\1\320\14}\215-I\224(\211"
+    "\222d\7\321\15}\215D\211\22\245\242$J\24\3\322\14}\35\251\22%Q\22E:\0\323\14}\35"
+    "\251\22%Q\22E:\0\324\14}\35\251\22%Q\22E:\0\325\14}\35\251\22%Q\22E:\0"
+    "\326\14}\215(\222*Q\22E:\0\327\11}},\212\64\251\14\330\14}\235%Q\22K\242$;"
+    "\0\331\15}\215(\211\222(\211\222(\322\1\332\15}\215(\211\222(\211\222(\322\1\333\15}\215("
+    "\211\222(\211\222(\322\1\334\13}\215(\216\222(\211\42\35\335\12}\235R\22%YX\7\336\12}"
+    "\215p\252La\16\1\337\12}\35\251R\252DI\35\340\13}\235\64[\242$\221\222\30\341\13}\255"
+    ",\134\242$\221\222\30\342\14}\255,\211\226(I\244$\6\343\13}\235nK\224$R\22\3\344\13"
+    "}\235\372\22%\211\224\304\0\345\13}\35M[\242$\221\222\30\346\11}}pI\224\332\14\347\11}"
+    "}T\12S-\2\350\11}\235\64\223\22\243\16\351\11}\255,\224\22\243\16\352\12}\235,\311\244\304"
+    "\250\3\353\12}\215$G\244\304\250\3\354\11}\235\64\23k\63\0\355\11}\255,\24k\63\0\356\11"
+    "}\255,\211\304\332\14\357\10}\235\272X\233\1\360\12}\235T\222*Q\244\3\361\12}\235NS%"
+    "J\242\30\362\12}\235\64\223*Q\244\3\363\12}\255,\224*Q\244\3\364\12}\35\35\221*Q\244"
+    "\3\365\12}\235nR%\212t\0\366\11}\235\272T\211\42\35\367\10}m}\310u\0\370\11}}"
+    "pI,\311\16\371\13}\235\64\252DI\24\315\0\372\13}\255\254\224DI\24\315\0\373\13}\35="
+    "J\242$\212f\0\374\13}\235r\224DI\24\315\0\375\12}\255\254\224D\225\254\6\376\12}\335p"
+    "\252D\311\24\2\377\12}\235r\224D\225\254\6\0\0\0\4\377\377\0";
+
+#endif /* U8G2_FONT_5X7_H */

+ 339 - 0
malveke_gb_photo/views/boilerplate_scene_1.c

@@ -0,0 +1,339 @@
+#include "../malveke_gb_photo.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+#include <dolphin/dolphin.h>
+
+extern const uint8_t u8g2_font_5x7_mf[1911]; // Declare as extern
+
+const uint8_t _I_DolphinCommon_56x48_0[] = {
+    0x01, 0x00, 0xdf, 0x00, 0x00, 0x1f, 0xfe, 0x0e, 0x05, 0x3f, 0x04, 0x06, 0x78, 0x06, 0x30, 0x20,
+    0xf8, 0x00, 0xc6, 0x12, 0x1c, 0x04, 0x0c, 0x0a, 0x38, 0x08, 0x08, 0x0c, 0x60, 0xc0, 0x21, 0xe0,
+    0x04, 0x0a, 0x18, 0x02, 0x1b, 0x00, 0x18, 0xa3, 0x00, 0x21, 0x90, 0x01, 0x8a, 0x20, 0x02, 0x19,
+    0x80, 0x18, 0x80, 0x64, 0x09, 0x20, 0x89, 0x81, 0x8c, 0x3e, 0x41, 0xe2, 0x80, 0x50, 0x00, 0x43,
+    0x08, 0x01, 0x0c, 0xfc, 0x68, 0x40, 0x61, 0xc0, 0x50, 0x30, 0x00, 0x63, 0xa0, 0x7f, 0x80, 0xc4,
+    0x41, 0x19, 0x07, 0xff, 0x02, 0x06, 0x18, 0x24, 0x03, 0x41, 0xf3, 0x2b, 0x10, 0x19, 0x38, 0x10,
+    0x30, 0x31, 0x7f, 0xe0, 0x34, 0x08, 0x30, 0x19, 0x60, 0x80, 0x65, 0x86, 0x0a, 0x4c, 0x0c, 0x30,
+    0x81, 0xb9, 0x41, 0xa0, 0x54, 0x08, 0xc7, 0xe2, 0x06, 0x8a, 0x18, 0x25, 0x02, 0x21, 0x0f, 0x19,
+    0x88, 0xd8, 0x6e, 0x1b, 0x01, 0xd1, 0x1b, 0x86, 0x39, 0x66, 0x3a, 0xa4, 0x1a, 0x50, 0x06, 0x48,
+    0x18, 0x18, 0xd0, 0x03, 0x01, 0x41, 0x98, 0xcc, 0x60, 0x39, 0x01, 0x49, 0x2d, 0x06, 0x03, 0x50,
+    0xf8, 0x40, 0x3e, 0x02, 0xc1, 0x82, 0x86, 0xc7, 0xfe, 0x0f, 0x28, 0x2c, 0x91, 0xd2, 0x90, 0x9a,
+    0x18, 0x19, 0x3e, 0x6d, 0x73, 0x12, 0x16, 0x00, 0x32, 0x49, 0x72, 0xc0, 0x7e, 0x5d, 0x44, 0xba,
+    0x2c, 0x08, 0xa4, 0xc8, 0x82, 0x06, 0x17, 0xe0, 0x81, 0x90, 0x2a, 0x40, 0x61, 0xe1, 0xa2, 0x44,
+    0x0c, 0x76, 0x2b, 0xe8, 0x89, 0x26, 0x43, 0x83, 0x31, 0x8c, 0x78, 0x0c, 0xb0, 0x48, 0x10, 0x1a,
+    0xe0, 0x00, 0x63,
+};
+const uint8_t* const _I_DolphinCommon_56x48[] = {_I_DolphinCommon_56x48_0};
+const Icon I_DolphinCommon_56x48 = {
+    .width = 56,
+    .height = 48,
+    .frame_count = 1,
+    .frame_rate = 0,
+    .frames = _I_DolphinCommon_56x48};
+
+struct BoilerplateScene1 {
+    View* view;
+    BoilerplateScene1Callback callback;
+    void* context;
+};
+
+typedef struct {
+    BoilerplateScene1* instance;
+} BoilerplateScene1Model;
+
+void boilerplate_scene_1_set_callback(
+    BoilerplateScene1* instance,
+    BoilerplateScene1Callback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+void boilerplate_scene_1_draw(Canvas* canvas, BoilerplateScene1Model* model) {
+    UNUSED(model);
+    BoilerplateScene1* instance = model->instance;
+    Boilerplate* app = instance->context;
+    canvas_clear(canvas);
+    // Prepare canvas
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_frame(canvas, 0, 0, FRAME_WIDTH, FRAME_HEIGTH);
+
+    if(app->show_instructions) {
+        canvas_draw_icon(canvas, 71, 15, &I_DolphinCommon_56x48);
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str_aligned(canvas, (128 / 2), 4, AlignCenter, AlignTop, "INSTRUCTIONS");
+
+        canvas_set_custom_u8g2_font(canvas, u8g2_font_5x7_mf); // 5x7 font, 9 lines
+
+        canvas_draw_icon_ex(canvas, 15, 18, &I_arrow_13x6, IconRotation180); // DOWN
+        canvas_draw_icon(canvas, 28, 18, &I_arrow_13x6); // UP
+        canvas_draw_str_aligned(canvas, 47, 21, AlignLeft, AlignCenter, "SCROLL");
+        canvas_draw_icon_ex(canvas, 33, 26, &I_arrow_13x6, IconRotation270); // PREV
+        canvas_draw_icon_ex(canvas, 22, 36, &I_arrow_13x6, IconRotation90); // NEXT
+        canvas_draw_str_aligned(canvas, 47, 32, AlignLeft, AlignCenter, "LEFT");
+        canvas_draw_str_aligned(canvas, 47, 43, AlignLeft, AlignCenter, "RIGHT");
+
+        canvas_set_font(canvas, FontPrimary);
+        elements_button_center(canvas, "OK");
+    } else {
+        // int count = (app->page) * 0x1000;
+
+        int count = GB_FIRST_PHOTO_OFFSET + app->page * GB_PHOTO_SIZE;
+
+        FURI_LOG_I(TAG, "Page \"%d\"\n", app->page);
+        FURI_LOG_I(TAG, "Read Index \"%d\" \n", count);
+
+        if(app->camera_ram_sav) {
+            storage_file_seek(app->camera_ram_sav, (app->page) + 0x11B2, true);
+            uint8_t status;
+            storage_file_read(app->camera_ram_sav, &status, 1);
+            FURI_LOG_I(TAG, "Status/Nº Photo \"%x\"\n", status);
+            storage_file_seek(app->camera_ram_sav, count, true);
+
+            for(int y = app->pos_y; y < 14; y++) {
+                for(int x = app->pos_x; x < 16; x++) {
+                    storage_file_read(app->camera_ram_sav, app->tile_data, sizeof(app->tile_data));
+                    for(int row = 0; row < 8; row++) {
+                        uint8_t temp1 = app->tile_data[row * 2];
+                        uint8_t temp2 = app->tile_data[row * 2 + 1];
+                        for(int pixel = 7; pixel >= 0; pixel--) {
+                            if(((temp1 & 1) + ((temp2 & 1) * 2)) >= 2) {
+                                canvas_draw_dot(canvas, (x * 8) + pixel, (y * 8) + row);
+                            }
+                            temp1 >>= 1;
+                            temp2 >>= 1;
+                        }
+                    }
+                }
+            }
+
+            if(app->info) {
+                if(status == 0xFF) { // DELETED
+                    canvas_draw_rbox(canvas, 100, 4, 20, 11, 4);
+                    canvas_invert_color(canvas);
+                    canvas_draw_str_aligned(canvas, 110, 10, AlignCenter, AlignCenter, "D");
+                    canvas_invert_color(canvas);
+                }
+            }
+        }
+    }
+}
+
+static void boilerplate_scene_1_model_init(BoilerplateScene1Model* const model, void* context) {
+    BoilerplateScene1* instance = context;
+    UNUSED(model);
+    UNUSED(instance);
+    model->instance = context;
+}
+
+void save_image(void* context) {
+    Boilerplate* app = context;
+    furi_assert(app);
+    NotificationApp* notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    //  Create MALVEKE dir
+    if(storage_common_stat(app->storage, MALVEKE_APP_FOLDER, NULL) == FSE_NOT_EXIST) {
+        storage_simply_mkdir(app->storage, MALVEKE_APP_FOLDER);
+    }
+    //  Create MALVEKE Photos dir
+    if(storage_common_stat(app->storage, MALVEKE_APP_FOLDER_PHOTOS, NULL) == FSE_NOT_EXIST) {
+        storage_simply_mkdir(app->storage, MALVEKE_APP_FOLDER_PHOTOS);
+    }
+
+    int count = GB_FIRST_PHOTO_OFFSET + app->page * GB_PHOTO_SIZE;
+    storage_file_seek(app->camera_ram_sav, count, true);
+    // create file name
+    FuriString* file_name = furi_string_alloc();
+    get_timefilename(file_name, app->page);
+
+    File* file = storage_file_alloc(app->storage);
+    bool result =
+        storage_file_open(file, furi_string_get_cstr(file_name), FSAM_WRITE, FSOM_OPEN_ALWAYS);
+
+    if(result) {
+        static char bmp[BMP_SIZE(WIDTH, HEIGHT)];
+        bmp_init(bmp, WIDTH, HEIGHT);
+
+        //  Selected Palette
+        uint32_t palette[] = {
+            bmp_encode(app->palette_color_hex_a),
+            bmp_encode(app->palette_color_hex_b),
+            bmp_encode(app->palette_color_hex_c),
+            bmp_encode(app->palette_color_hex_d)};
+
+        UNUSED(palette);
+
+        for(int y = 0; y < 14; y++) {
+            for(int x = 0; x < 16; x++) {
+                storage_file_read(app->camera_ram_sav, app->tile_data, sizeof(app->tile_data));
+                for(int row = 0; row < 8; row++) {
+                    uint8_t temp1 = app->tile_data[row * 2];
+                    uint8_t temp2 = app->tile_data[row * 2 + 1];
+
+                    for(int pixel = 7; pixel >= 0; pixel--) {
+                        bmp_set(
+                            bmp,
+                            (x * 8) + pixel,
+                            (y * 8) + row,
+                            palette[((temp1 & 1) + ((temp2 & 1) * 2))]);
+                        temp1 >>= 1;
+                        temp2 >>= 1;
+                    }
+                }
+            }
+        }
+
+        storage_file_write(file, bmp, sizeof(bmp));
+        storage_file_close(file);
+    }
+
+    // Closing the "file descriptor"
+    storage_file_close(file);
+    // Freeing up memory
+    storage_file_free(file);
+    notification_message(notifications, result ? &sequence_success : &sequence_error);
+}
+bool boilerplate_scene_1_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    BoilerplateScene1* instance = context;
+    Boilerplate* app = instance->context;
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyBack:
+            with_view_model(
+                instance->view,
+                BoilerplateScene1Model * model,
+                {
+                    UNUSED(model);
+                    instance->callback(BoilerplateCustomEventScene1Back, instance->context);
+                },
+                true);
+            break;
+        case InputKeyLeft:
+            with_view_model(
+                instance->view,
+                BoilerplateScene1Model * model,
+                {
+                    UNUSED(model);
+                    app->pos_y = 0;
+                    app->pos_x = 0;
+
+                    if(app->page > 0) {
+                        app->page--;
+                    } else {
+                        app->page = 29;
+                    }
+                },
+                true);
+            break;
+        case InputKeyRight:
+            with_view_model(
+                instance->view,
+                BoilerplateScene1Model * model,
+                {
+                    UNUSED(model);
+                    app->pos_y = 0;
+                    app->pos_x = 0;
+                    if(app->page < 29) {
+                        app->page++;
+                    } else {
+                        app->page = 0;
+                    }
+                },
+                true);
+            break;
+        case InputKeyUp:
+            with_view_model(
+                instance->view,
+                BoilerplateScene1Model * model,
+                {
+                    UNUSED(model);
+                    if(app->pos_y < 0) {
+                        app->pos_y++;
+                    }
+                },
+                true);
+            break;
+        case InputKeyDown:
+            with_view_model(
+                instance->view,
+                BoilerplateScene1Model * model,
+                {
+                    UNUSED(model);
+                    if(app->pos_y > -6) {
+                        app->pos_y--;
+                    }
+                },
+                true);
+            break;
+        case InputKeyOk:
+            with_view_model(
+                instance->view,
+                BoilerplateScene1Model * model,
+                {
+                    UNUSED(model);
+                    if(app->show_instructions) {
+                        app->show_instructions = false;
+                    } else {
+                        save_image(app);
+                    }
+                },
+                true);
+            break;
+        case InputKeyMAX:
+            break;
+        }
+    }
+    return true;
+}
+
+void boilerplate_scene_1_exit(void* context) {
+    furi_assert(context);
+}
+
+void boilerplate_scene_1_enter(void* context) {
+    furi_assert(context);
+    BoilerplateScene1* instance = (BoilerplateScene1*)context;
+
+    with_view_model(
+        instance->view,
+        BoilerplateScene1Model * model,
+        { boilerplate_scene_1_model_init(model, instance); },
+        true);
+}
+
+BoilerplateScene1* boilerplate_scene_1_alloc() {
+    BoilerplateScene1* instance = malloc(sizeof(BoilerplateScene1));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(BoilerplateScene1Model));
+    view_set_context(instance->view, instance); // furi_assert crashes in events without this
+    view_set_draw_callback(instance->view, (ViewDrawCallback)boilerplate_scene_1_draw);
+    view_set_input_callback(instance->view, boilerplate_scene_1_input);
+    view_set_enter_callback(instance->view, boilerplate_scene_1_enter);
+    view_set_exit_callback(instance->view, boilerplate_scene_1_exit);
+
+    with_view_model(
+        instance->view,
+        BoilerplateScene1Model * model,
+        { boilerplate_scene_1_model_init(model, instance); },
+        true);
+
+    return instance;
+}
+
+void boilerplate_scene_1_free(BoilerplateScene1* instance) {
+    furi_assert(instance);
+
+    with_view_model(
+        instance->view, BoilerplateScene1Model * model, { UNUSED(model); }, true);
+    view_free(instance->view);
+    free(instance);
+}
+
+View* boilerplate_scene_1_get_view(BoilerplateScene1* instance) {
+    furi_assert(instance);
+    return instance->view;
+}

+ 30 - 0
malveke_gb_photo/views/boilerplate_scene_1.h

@@ -0,0 +1,30 @@
+#pragma once
+
+#include <gui/view.h>
+#include <gui/icon_i.h>
+#include "../helpers/boilerplate_custom_event.h"
+#include <malveke_gb_photo_icons.h>
+#define BMP_COMPAT // write bottom to top
+#include "../helpers/bmp.h"
+#include "../helpers/malveke_photo.h"
+
+#define WIDTH 128L
+#define HEIGHT 112L
+
+#define FRAME_WIDTH 128
+#define FRAME_HEIGTH 64
+
+typedef struct BoilerplateScene1 BoilerplateScene1;
+
+typedef void (*BoilerplateScene1Callback)(BoilerplateCustomEvent event, void* context);
+
+void boilerplate_scene_1_set_callback(
+    BoilerplateScene1* boilerplate_scene_1,
+    BoilerplateScene1Callback callback,
+    void* context);
+
+View* boilerplate_scene_1_get_view(BoilerplateScene1* boilerplate_static);
+
+BoilerplateScene1* boilerplate_scene_1_alloc();
+
+void boilerplate_scene_1_free(BoilerplateScene1* boilerplate_static);

+ 247 - 0
malveke_gb_photo/views/boilerplate_scene_2.c

@@ -0,0 +1,247 @@
+#include "../malveke_gb_photo.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+#include <dolphin/dolphin.h>
+
+struct BoilerplateScene2 {
+    View* view;
+    BoilerplateScene2Callback callback;
+    void* context;
+};
+
+typedef struct {
+    bool in_progress;
+    int page;
+    BoilerplateScene2* instance;
+} BoilerplateScene2Model;
+
+void boilerplate_scene_2_set_callback(
+    BoilerplateScene2* instance,
+    BoilerplateScene2Callback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+void draw_thumbnail(void* context, Canvas* canvas, int page) {
+    BoilerplateScene2* instance = context;
+    Boilerplate* app = instance->context;
+    UNUSED(app);
+    //  Gallery
+    // for (int s=0;s< 8 * (page+1);s++) {
+    // int count = ((page + 2) * 0x1000) + 0x0e00;
+    int count = ((page + 2) * GB_PHOTO_SIZE) + 0x0e00;
+
+    storage_file_seek(app->camera_ram_sav, count, true);
+    for(int y = 0; y < 4; y++) {
+        for(int x = 0; x < 4; x++) {
+            storage_file_read(app->camera_ram_sav, app->tile_data, sizeof(app->tile_data));
+            for(int row = 0; row < 8; row++) {
+                uint8_t temp1 = app->tile_data[row * 2];
+                uint8_t temp2 = app->tile_data[row * 2 + 1];
+                for(int pixel = 7; pixel >= 0; pixel--) {
+                    int colorIndex = ((temp1 & 1) + ((temp2 & 1) * 2));
+                    if(colorIndex >= 2) {
+                        canvas_draw_dot(canvas, (x * 8) + (pixel + 47), (y * 8) + row + 17);
+                    }
+                    temp1 >>= 1;
+                    temp2 >>= 1;
+                }
+            }
+        }
+    }
+    // }
+}
+void boilerplate_scene_2_draw(Canvas* canvas, BoilerplateScene2Model* model) {
+    UNUSED(model);
+    BoilerplateScene2* instance = model->instance;
+    Boilerplate* app = instance->context;
+    UNUSED(app);
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 21, 13, "Export All Photos");
+    canvas_set_font(canvas, FontPrimary);
+
+    char totalText[32];
+    snprintf(totalText, sizeof(totalText), "%02d/29", model->page);
+
+    draw_thumbnail(instance, canvas, model->page);
+    if(!model->in_progress) {
+        elements_button_center(canvas, "OK");
+    } else {
+        float dict_progress = (float)(model->page * 1) / (float)(29);
+        int progress_width = 128 / 2;
+        int position_x = (128 / 2);
+        int position_y = 52;
+        elements_progress_bar_with_text(
+            canvas,
+            position_x - (progress_width / 2),
+            position_y,
+            progress_width,
+            dict_progress,
+            totalText);
+    }
+}
+
+static void boilerplate_scene_2_model_init(BoilerplateScene2Model* const model, void* context) {
+    BoilerplateScene2* instance = context;
+    model->instance = instance;
+    model->page = 0;
+    model->in_progress = false;
+}
+void save_all_image(void* context) {
+    BoilerplateScene2* instance = context;
+    Boilerplate* app = instance->context;
+    UNUSED(app);
+    furi_assert(app);
+    NotificationApp* notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    //  Create MALVEKE dir
+    if(storage_common_stat(app->storage, MALVEKE_APP_FOLDER, NULL) == FSE_NOT_EXIST) {
+        storage_simply_mkdir(app->storage, MALVEKE_APP_FOLDER);
+    }
+    //  Create MALVEKE Photos dir
+    if(storage_common_stat(app->storage, MALVEKE_APP_FOLDER_PHOTOS, NULL) == FSE_NOT_EXIST) {
+        storage_simply_mkdir(app->storage, MALVEKE_APP_FOLDER_PHOTOS);
+    }
+
+    for(int page = 0; page < 30; page++) {
+        int count = GB_FIRST_PHOTO_OFFSET + page * GB_PHOTO_SIZE;
+        storage_file_seek(app->camera_ram_sav, count, true);
+        // create file name
+        FuriString* file_name = furi_string_alloc();
+        get_timefilename(file_name, page);
+
+        File* file = storage_file_alloc(app->storage);
+        bool result =
+            storage_file_open(file, furi_string_get_cstr(file_name), FSAM_WRITE, FSOM_OPEN_ALWAYS);
+
+        if(result) {
+            static char bmp[BMP_SIZE(WIDTH, HEIGHT)];
+            bmp_init(bmp, WIDTH, HEIGHT);
+
+            //  Selected Palette
+            uint32_t palette[] = {
+                bmp_encode(app->palette_color_hex_a),
+                bmp_encode(app->palette_color_hex_b),
+                bmp_encode(app->palette_color_hex_c),
+                bmp_encode(app->palette_color_hex_d)};
+
+            UNUSED(palette);
+
+            for(int y = 0; y < 14; y++) {
+                for(int x = 0; x < 16; x++) {
+                    storage_file_read(app->camera_ram_sav, app->tile_data, sizeof(app->tile_data));
+                    for(int row = 0; row < 8; row++) {
+                        uint8_t temp1 = app->tile_data[row * 2];
+                        uint8_t temp2 = app->tile_data[row * 2 + 1];
+
+                        for(int pixel = 7; pixel >= 0; pixel--) {
+                            bmp_set(
+                                bmp,
+                                (x * 8) + pixel,
+                                (y * 8) + row,
+                                palette[((temp1 & 1) + ((temp2 & 1) * 2))]);
+                            temp1 >>= 1;
+                            temp2 >>= 1;
+                        }
+                    }
+                }
+            }
+
+            storage_file_write(file, bmp, sizeof(bmp));
+
+            with_view_model(
+                instance->view,
+                BoilerplateScene2Model * model,
+                {
+                    // UNUSED(model);
+                    model->page = page;
+                },
+                true);
+            furi_delay_ms(120);
+        }
+
+        // Closing the "file descriptor"
+        storage_file_close(file);
+        // Freeing up memory
+        storage_file_free(file);
+    }
+    notification_message(notifications, &sequence_success);
+    with_view_model(
+        instance->view, BoilerplateScene2Model * model, { model->in_progress = false; }, true);
+}
+bool boilerplate_scene_2_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    BoilerplateScene2* instance = context;
+    // Boilerplate *app = instance->context;
+
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyOk:
+            with_view_model(
+                instance->view,
+                BoilerplateScene2Model * model,
+                { model->in_progress = true; },
+                true);
+
+            save_all_image(instance);
+            break;
+        case InputKeyBack:
+            instance->callback(BoilerplateCustomEventScene2Back, instance->context);
+            break;
+        case InputKeyLeft:
+        case InputKeyRight:
+        case InputKeyUp:
+        case InputKeyDown:
+        case InputKeyMAX:
+            break;
+        }
+    }
+
+    return true;
+}
+
+void boilerplate_scene_2_exit(void* context) {
+    furi_assert(context);
+    Boilerplate* app = context;
+    UNUSED(app);
+    // boilerplate_stop_all_sound(app);
+    // boilerplate_led_reset(app);
+}
+
+BoilerplateScene2* boilerplate_scene_2_alloc() {
+    BoilerplateScene2* instance = malloc(sizeof(BoilerplateScene2));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(BoilerplateScene2Model));
+    view_set_context(instance->view, instance);
+    view_set_draw_callback(instance->view, (ViewDrawCallback)boilerplate_scene_2_draw);
+    view_set_input_callback(instance->view, boilerplate_scene_2_input);
+    view_set_exit_callback(instance->view, boilerplate_scene_2_exit);
+
+    with_view_model(
+        instance->view,
+        BoilerplateScene2Model * model,
+        { boilerplate_scene_2_model_init(model, instance); },
+        true);
+
+    return instance;
+}
+
+void boilerplate_scene_2_free(BoilerplateScene2* instance) {
+    furi_assert(instance);
+
+    view_free(instance->view);
+    free(instance);
+}
+
+View* boilerplate_scene_2_get_view(BoilerplateScene2* instance) {
+    furi_assert(instance);
+
+    return instance->view;
+}

+ 22 - 0
malveke_gb_photo/views/boilerplate_scene_2.h

@@ -0,0 +1,22 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/boilerplate_custom_event.h"
+#define BMP_COMPAT // write bottom to top
+#include "../helpers/bmp.h"
+#include "../helpers/malveke_photo.h"
+
+typedef struct BoilerplateScene2 BoilerplateScene2;
+
+typedef void (*BoilerplateScene2Callback)(BoilerplateCustomEvent event, void* context);
+
+void boilerplate_scene_2_set_callback(
+    BoilerplateScene2* instance,
+    BoilerplateScene2Callback callback,
+    void* context);
+
+BoilerplateScene2* boilerplate_scene_2_alloc();
+
+void boilerplate_scene_2_free(BoilerplateScene2* boilerplate_static);
+
+View* boilerplate_scene_2_get_view(BoilerplateScene2* boilerpate_static);

+ 116 - 0
malveke_gb_photo/views/boilerplate_startscreen.c

@@ -0,0 +1,116 @@
+#include "../malveke_gb_photo.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <input/input.h>
+#include <gui/elements.h>
+
+extern const uint8_t u8g2_font_5x7_mf[1911]; // Declare as extern
+
+struct BoilerplateStartscreen {
+    View* view;
+    BoilerplateStartscreenCallback callback;
+    void* context;
+};
+
+typedef struct {
+    int some_value;
+} BoilerplateStartscreenModel;
+
+void boilerplate_startscreen_set_callback(
+    BoilerplateStartscreen* instance,
+    BoilerplateStartscreenCallback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+    instance->callback = callback;
+    instance->context = context;
+}
+
+void boilerplate_startscreen_draw(Canvas* canvas, BoilerplateStartscreenModel* model) {
+    UNUSED(model);
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(canvas, 64, 10, AlignCenter, AlignTop, "GAME BOY PHOTO");
+    canvas_set_custom_u8g2_font(canvas, u8g2_font_5x7_mf);
+    canvas_draw_str_aligned(canvas, 64, 28, AlignCenter, AlignTop, "SELECT A '.SAV' FILE ");
+    canvas_draw_str_aligned(canvas, 64, 38, AlignCenter, AlignTop, "FROM GB CAMERA TO PROCEED");
+    canvas_set_font(canvas, FontSecondary);
+    elements_button_center(canvas, "Start");
+}
+
+static void boilerplate_startscreen_model_init(BoilerplateStartscreenModel* const model) {
+    model->some_value = 1;
+}
+
+bool boilerplate_startscreen_input(InputEvent* event, void* context) {
+    furi_assert(context);
+    BoilerplateStartscreen* instance = context;
+    if(event->type == InputTypeRelease) {
+        switch(event->key) {
+        case InputKeyBack:
+            with_view_model(
+                instance->view,
+                BoilerplateStartscreenModel * model,
+                {
+                    UNUSED(model);
+                    instance->callback(BoilerplateCustomEventStartscreenBack, instance->context);
+                },
+                true);
+            break;
+        case InputKeyLeft:
+        case InputKeyRight:
+        case InputKeyUp:
+        case InputKeyDown:
+        case InputKeyOk:
+            instance->callback(BoilerplateCustomEventStartscreenOk, instance->context);
+            break;
+        case InputKeyMAX:
+            break;
+        }
+    }
+    return true;
+}
+
+void boilerplate_startscreen_exit(void* context) {
+    furi_assert(context);
+}
+
+void boilerplate_startscreen_enter(void* context) {
+    furi_assert(context);
+    BoilerplateStartscreen* instance = (BoilerplateStartscreen*)context;
+    with_view_model(
+        instance->view,
+        BoilerplateStartscreenModel * model,
+        { boilerplate_startscreen_model_init(model); },
+        true);
+}
+
+BoilerplateStartscreen* boilerplate_startscreen_alloc() {
+    BoilerplateStartscreen* instance = malloc(sizeof(BoilerplateStartscreen));
+    instance->view = view_alloc();
+    view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(BoilerplateStartscreenModel));
+    view_set_context(instance->view, instance); // furi_assert crashes in events without this
+    view_set_draw_callback(instance->view, (ViewDrawCallback)boilerplate_startscreen_draw);
+    view_set_input_callback(instance->view, boilerplate_startscreen_input);
+    with_view_model(
+        instance->view,
+        BoilerplateStartscreenModel * model,
+        { boilerplate_startscreen_model_init(model); },
+        true);
+
+    return instance;
+}
+
+void boilerplate_startscreen_free(BoilerplateStartscreen* instance) {
+    furi_assert(instance);
+    with_view_model(
+        instance->view, BoilerplateStartscreenModel * model, { UNUSED(model); }, true);
+    view_free(instance->view);
+    free(instance);
+}
+
+View* boilerplate_startscreen_get_view(BoilerplateStartscreen* instance) {
+    furi_assert(instance);
+    return instance->view;
+}

+ 19 - 0
malveke_gb_photo/views/boilerplate_startscreen.h

@@ -0,0 +1,19 @@
+#pragma once
+
+#include <gui/view.h>
+#include "../helpers/boilerplate_custom_event.h"
+
+typedef struct BoilerplateStartscreen BoilerplateStartscreen;
+
+typedef void (*BoilerplateStartscreenCallback)(BoilerplateCustomEvent event, void* context);
+
+void boilerplate_startscreen_set_callback(
+    BoilerplateStartscreen* boilerplate_startscreen,
+    BoilerplateStartscreenCallback callback,
+    void* context);
+
+View* boilerplate_startscreen_get_view(BoilerplateStartscreen* boilerplate_static);
+
+BoilerplateStartscreen* boilerplate_startscreen_alloc();
+
+void boilerplate_startscreen_free(BoilerplateStartscreen* boilerplate_static);