1// Aseprite
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/app.h"
13
14#include "app/app_mod.h"
15#include "app/check_update.h"
16#include "app/cli/app_options.h"
17#include "app/cli/cli_processor.h"
18#include "app/cli/default_cli_delegate.h"
19#include "app/cli/preview_cli_delegate.h"
20#include "app/color_spaces.h"
21#include "app/color_utils.h"
22#include "app/commands/commands.h"
23#include "app/console.h"
24#include "app/crash/data_recovery.h"
25#include "app/drm.h"
26#include "app/extensions.h"
27#include "app/file/file.h"
28#include "app/file/file_formats_manager.h"
29#include "app/file_system.h"
30#include "app/gui_xml.h"
31#include "app/i18n/strings.h"
32#include "app/ini_file.h"
33#include "app/log.h"
34#include "app/modules.h"
35#include "app/modules/gfx.h"
36#include "app/modules/gui.h"
37#include "app/modules/palettes.h"
38#include "app/pref/preferences.h"
39#include "app/recent_files.h"
40#include "app/resource_finder.h"
41#include "app/send_crash.h"
42#include "app/site.h"
43#include "app/tools/active_tool.h"
44#include "app/tools/tool_box.h"
45#include "app/ui/backup_indicator.h"
46#include "app/ui/color_bar.h"
47#include "app/ui/doc_view.h"
48#include "app/ui/editor/editor.h"
49#include "app/ui/editor/editor_view.h"
50#include "app/ui/input_chain.h"
51#include "app/ui/keyboard_shortcuts.h"
52#include "app/ui/main_window.h"
53#include "app/ui/status_bar.h"
54#include "app/ui/toolbar.h"
55#include "app/ui/workspace.h"
56#include "app/ui_context.h"
57#include "app/util/clipboard.h"
58#include "base/exception.h"
59#include "base/fs.h"
60#include "base/split_string.h"
61#include "doc/sprite.h"
62#include "fmt/format.h"
63#include "os/error.h"
64#include "os/surface.h"
65#include "os/system.h"
66#include "os/window.h"
67#include "render/render.h"
68#include "ui/intern.h"
69#include "ui/ui.h"
70#include "updater/user_agent.h"
71#include "ver/info.h"
72
73#if LAF_MACOS
74 #include "os/osx/system.h"
75#elif LAF_LINUX
76 #include "os/x11/system.h"
77#endif
78
79#include <iostream>
80#include <memory>
81
82#ifdef ENABLE_SCRIPTING
83 #include "app/script/engine.h"
84 #include "app/shell.h"
85#endif
86
87#ifdef ENABLE_STEAM
88 #include "steam/steam.h"
89#endif
90
91#include <memory>
92#include <optional>
93
94namespace app {
95
96using namespace ui;
97
98#ifdef ENABLE_SCRIPTING
99
100namespace {
101
102class ConsoleEngineDelegate : public script::EngineDelegate {
103public:
104 ConsoleEngineDelegate(Console& console) : m_console(console) { }
105 void onConsoleError(const char* text) override {
106 onConsolePrint(text);
107 }
108 void onConsolePrint(const char* text) override {
109 m_console.printf("%s\n", text);
110 }
111private:
112 Console& m_console;
113};
114
115} // anonymous namespace
116
117#endif // ENABLER_SCRIPTING
118
119class App::CoreModules {
120public:
121#ifdef ENABLE_UI
122 typedef app::UIContext ContextT;
123#else
124 typedef app::Context ContextT;
125#endif
126
127 ConfigModule m_configModule;
128 ContextT m_context;
129};
130
131class App::LoadLanguage {
132public:
133 LoadLanguage(Preferences& pref,
134 Extensions& exts) {
135 Strings::createInstance(pref, exts);
136 }
137};
138
139class App::Modules {
140public:
141 LoggerModule m_loggerModule;
142 FileSystemModule m_file_system_module;
143 Extensions m_extensions;
144 // Load main language (after loading the extensions)
145 LoadLanguage m_loadLanguage;
146 tools::ToolBox m_toolbox;
147 tools::ActiveToolManager m_activeToolManager;
148 Commands m_commands;
149#ifdef ENABLE_UI
150 RecentFiles m_recent_files;
151 InputChain m_inputChain;
152 Clipboard m_clipboard;
153#endif
154#ifdef ENABLE_DATA_RECOVERY
155 // This is a raw pointer because we want to delete it explicitly.
156 // (e.g. if an exception occurs, the ~Modules() doesn't have to
157 // delete m_recovery)
158 std::unique_ptr<app::crash::DataRecovery> m_recovery;
159#endif
160
161 Modules(const bool createLogInDesktop,
162 Preferences& pref)
163 : m_loggerModule(createLogInDesktop)
164 , m_loadLanguage(pref, m_extensions)
165 , m_activeToolManager(&m_toolbox)
166#ifdef ENABLE_UI
167 , m_recent_files(pref.general.recentItems())
168#endif
169#ifdef ENABLE_DATA_RECOVERY
170 , m_recovery(nullptr)
171#endif
172 {
173 }
174
175 ~Modules() {
176#ifdef ENABLE_DATA_RECOVERY
177 ASSERT(m_recovery == nullptr ||
178 ui::get_app_state() == ui::AppState::kClosingWithException);
179#endif
180 }
181
182 app::crash::DataRecovery* recovery() {
183#ifdef ENABLE_DATA_RECOVERY
184 return m_recovery.get();
185#else
186 return nullptr;
187#endif
188 }
189
190 void createDataRecovery(Context* ctx) {
191#ifdef ENABLE_DATA_RECOVERY
192
193#ifdef ENABLE_TRIAL_MODE
194 DRM_INVALID{
195 return;
196 }
197#endif
198
199 m_recovery = std::make_unique<app::crash::DataRecovery>(ctx);
200 m_recovery->SessionsListIsReady.connect(
201 [] {
202 ui::assert_ui_thread();
203 auto app = App::instance();
204 if (app && app->mainWindow()) {
205 // Notify that the list of sessions is ready.
206 app->mainWindow()->dataRecoverySessionsAreReady();
207 }
208 });
209#endif
210 }
211
212 void searchDataRecoverySessions() {
213#ifdef ENABLE_DATA_RECOVERY
214
215#ifdef ENABLE_TRIAL_MODE
216 DRM_INVALID{
217 return;
218 }
219#endif
220
221 ASSERT(m_recovery);
222 if (m_recovery)
223 m_recovery->launchSearch();
224#endif
225 }
226
227 void deleteDataRecovery() {
228#ifdef ENABLE_DATA_RECOVERY
229
230#ifdef ENABLE_TRIAL_MODE
231 DRM_INVALID{
232 return;
233 }
234#endif
235
236 m_recovery.reset();
237#endif
238 }
239
240};
241
242App* App::m_instance = nullptr;
243
244App::App(AppMod* mod)
245 : m_mod(mod)
246 , m_coreModules(nullptr)
247 , m_modules(nullptr)
248 , m_legacy(nullptr)
249 , m_isGui(false)
250 , m_isShell(false)
251#ifdef ENABLE_UI
252 , m_backupIndicator(nullptr)
253#endif
254#ifdef ENABLE_SCRIPTING
255 , m_engine(new script::Engine)
256#endif
257{
258 ASSERT(m_instance == nullptr);
259 m_instance = this;
260}
261
262int App::initialize(const AppOptions& options)
263{
264 os::System* system = os::instance();
265
266#ifdef ENABLE_UI
267 m_isGui = options.startUI() && !options.previewCLI();
268#else
269 m_isGui = false;
270#endif
271 m_isShell = options.startShell();
272 m_coreModules = std::make_unique<CoreModules>();
273
274#if LAF_WINDOWS
275
276 if (options.disableWintab() ||
277 !preferences().experimental.loadWintabDriver() ||
278 preferences().tablet.api() == "pointer") {
279 system->setTabletAPI(os::TabletAPI::WindowsPointerInput);
280 }
281 else if (preferences().tablet.api() == "wintab_packets")
282 system->setTabletAPI(os::TabletAPI::WintabPackets);
283 else // preferences().tablet.api() == "wintab"
284 system->setTabletAPI(os::TabletAPI::Wintab);
285
286#elif LAF_MACOS
287
288 if (!preferences().general.osxAsyncView())
289 os::osx_set_async_view(false);
290
291#elif LAF_LINUX
292
293 {
294 const std::string& stylusId = preferences().general.x11StylusId();
295 if (!stylusId.empty())
296 os::x11_set_user_defined_string_to_detect_stylus(stylusId);
297 }
298
299#endif
300
301 system->setAppName(get_app_name());
302 system->setAppMode(m_isGui ? os::AppMode::GUI:
303 os::AppMode::CLI);
304
305 if (m_isGui)
306 m_uiSystem.reset(new ui::UISystem);
307
308 bool createLogInDesktop = false;
309 switch (options.verboseLevel()) {
310 case AppOptions::kNoVerbose:
311 base::set_log_level(ERROR);
312 break;
313 case AppOptions::kVerbose:
314 base::set_log_level(INFO);
315 break;
316 case AppOptions::kHighlyVerbose:
317 base::set_log_level(VERBOSE);
318 createLogInDesktop = true;
319 break;
320 }
321
322 initialize_color_spaces(preferences());
323
324#ifdef ENABLE_DRM
325 LOG("APP: Initializing DRM...\n");
326 app_configure_drm();
327#endif
328
329 // Load modules
330 m_modules = std::make_unique<Modules>(createLogInDesktop, preferences());
331 m_legacy = std::make_unique<LegacyModules>(isGui() ? REQUIRE_INTERFACE: 0);
332#ifdef ENABLE_UI
333 m_brushes = std::make_unique<AppBrushes>();
334#endif
335
336 // Data recovery is enabled only in GUI mode
337 if (isGui() && preferences().general.dataRecovery())
338 m_modules->createDataRecovery(context());
339
340 if (isPortable())
341 LOG("APP: Running in portable mode\n");
342
343 // Load or create the default palette, or migrate the default
344 // palette from an old format palette to the new one, etc.
345 load_default_palette();
346
347#ifdef ENABLE_UI
348 // Initialize GUI interface
349 if (isGui()) {
350 LOG("APP: GUI mode\n");
351
352 // Set the ClipboardDelegate impl to copy/paste text in the native
353 // clipboard from the ui::Entry control.
354 m_uiSystem->setClipboardDelegate(&m_modules->m_clipboard);
355
356 // Setup the GUI cursor and redraw screen
357 ui::set_use_native_cursors(preferences().cursor.useNativeCursor());
358 ui::set_mouse_cursor_scale(preferences().cursor.cursorScale());
359 ui::set_mouse_cursor(kArrowCursor);
360
361 auto manager = ui::Manager::getDefault();
362 manager->invalidate();
363
364 // Create the main window.
365 m_mainWindow.reset(new MainWindow);
366 if (m_mod)
367 m_mod->modMainWindow(m_mainWindow.get());
368
369 // Data recovery is enabled only in GUI mode
370 if (preferences().general.dataRecovery())
371 m_modules->searchDataRecoverySessions();
372
373 // Default status of the main window.
374 app_rebuild_documents_tabs();
375 m_mainWindow->statusBar()->showDefaultText();
376
377 // Show the main window (this is not modal, the code continues)
378 m_mainWindow->openWindow();
379
380#if LAF_LINUX // TODO check why this is required and we cannot call
381 // updateAllDisplaysWithNewScale() on Linux/X11
382 // Redraw the whole screen.
383 manager->invalidate();
384#else
385 // To know the initial manager size we call to
386 // Manager::updateAllDisplaysWithNewScale(...) so we receive a
387 // Manager::onNewDisplayConfiguration() (which will update the
388 // bounds of the manager for first time). This is required so if
389 // the OpenFileCommand (called when we're processing the CLI with
390 // OpenBatchOfFiles) shows a dialog to open a sequence of files,
391 // the dialog is centered correctly to the manager bounds.
392 const int scale = Preferences::instance().general.screenScale();
393 manager->updateAllDisplaysWithNewScale(scale);
394#endif
395 }
396#endif // ENABLE_UI
397
398#ifdef ENABLE_SCRIPTING
399 // Call the init() function from all plugins
400 LOG("APP: Initializing scripts...\n");
401 extensions().executeInitActions();
402#endif
403
404 // Process options
405 LOG("APP: Processing options...\n");
406 int code;
407 {
408 std::unique_ptr<CliDelegate> delegate;
409 if (options.previewCLI())
410 delegate.reset(new PreviewCliDelegate);
411 else
412 delegate.reset(new DefaultCliDelegate);
413
414 CliProcessor cli(delegate.get(), options);
415 code = cli.process(context());
416 }
417
418 LOG("APP: Finish launching...\n");
419 system->finishLaunching();
420 return code;
421}
422
423namespace {
424
425#ifdef ENABLE_UI
426 struct CloseMainWindow {
427 std::unique_ptr<MainWindow>& m_win;
428 CloseMainWindow(std::unique_ptr<MainWindow>& win) : m_win(win) { }
429 ~CloseMainWindow() { m_win.reset(nullptr); }
430 };
431#endif
432
433 struct CloseAllDocs {
434 Context* m_ctx;
435 CloseAllDocs(Context* ctx) : m_ctx(ctx) { }
436 ~CloseAllDocs() {
437 std::vector<Doc*> docs;
438#ifdef ENABLE_UI
439 for (Doc* doc : static_cast<UIContext*>(m_ctx)->getAndRemoveAllClosedDocs())
440 docs.push_back(doc);
441#endif
442 for (Doc* doc : m_ctx->documents())
443 docs.push_back(doc);
444 for (Doc* doc : docs) {
445 // First we close the document. In this way we receive recent
446 // notifications related to the document as a app::Doc. If
447 // we delete the document directly, we destroy the app::Doc
448 // too early, and then doc::~Document() call
449 // DocsObserver::onRemoveDocument(). In this way, observers
450 // could think that they have a fully created app::Doc when
451 // in reality it's a doc::Document (in the middle of a
452 // destruction process).
453 //
454 // TODO: This problem is because we're extending doc::Document,
455 // in the future, we should remove app::Doc.
456 doc->close();
457 delete doc;
458 }
459 }
460 };
461
462} // anonymous namespace
463
464void App::run()
465{
466#ifdef ENABLE_UI
467 CloseMainWindow closeMainWindow(m_mainWindow);
468#endif
469 CloseAllDocs closeAllDocsAtExit(context());
470
471#ifdef ENABLE_UI
472 // Run the GUI
473 if (isGui()) {
474 auto manager = ui::Manager::getDefault();
475#if LAF_WINDOWS
476 // How to interpret one finger on Windows tablets.
477 manager->display()->nativeWindow()
478 ->setInterpretOneFingerGestureAsMouseMovement(
479 preferences().experimental.oneFingerAsMouseMovement());
480#endif
481
482#if LAF_LINUX
483 // Setup app icon for Linux window managers
484 try {
485 os::Window* window = os::instance()->defaultWindow();
486 os::SurfaceList icons;
487
488 for (const int size : { 32, 64, 128 }) {
489 ResourceFinder rf;
490 rf.includeDataDir(fmt::format("icons/ase{0}.png", size).c_str());
491 if (rf.findFirst()) {
492 os::SurfaceRef surf = os::instance()->loadRgbaSurface(rf.filename().c_str());
493 if (surf) {
494 surf->setImmutable();
495 icons.push_back(surf);
496 }
497 }
498 }
499
500 window->setIcons(icons);
501 }
502 catch (const std::exception&) {
503 // Just ignore the exception, we couldn't change the app icon, no
504 // big deal.
505 }
506#endif
507
508 // Initialize Steam API
509#ifdef ENABLE_STEAM
510 steam::SteamAPI steam;
511 if (steam.initialized())
512 os::instance()->activateApp();
513#endif
514
515#if defined(_DEBUG) || defined(ENABLE_DEVMODE)
516 // On OS X, when we compile Aseprite on devmode, we're using it
517 // outside an app bundle, so we must active the app explicitly.
518 if (isGui())
519 os::instance()->activateApp();
520#endif
521
522#ifdef ENABLE_UPDATER
523 // Launch the thread to check for updates.
524 app::CheckUpdateThreadLauncher checkUpdate(
525 m_mainWindow->getCheckUpdateDelegate());
526 checkUpdate.launch();
527#endif
528
529#if !ENABLE_SENTRY
530 app::SendCrash sendCrash;
531 sendCrash.search();
532#endif
533
534 // Keep the console alive the whole program execute (just in case
535 // we've to print errors).
536 Console console;
537#ifdef ENABLE_SCRIPTING
538 // Use the app::Console() for script errors
539 ConsoleEngineDelegate delegate(console);
540 script::ScopedEngineDelegate setEngineDelegate(m_engine.get(), &delegate);
541#endif
542
543 // Run the GUI main message loop
544 try {
545 manager->run();
546 set_app_state(AppState::kClosing);
547 }
548 catch (...) {
549 set_app_state(AppState::kClosingWithException);
550 throw;
551 }
552 }
553#endif // ENABLE_UI
554
555#ifdef ENABLE_SCRIPTING
556 // Start shell to execute scripts.
557 if (m_isShell) {
558 m_engine->printLastResult(); // TODO is this needed?
559 Shell shell;
560 shell.run(*m_engine);
561 }
562#endif // ENABLE_SCRIPTING
563
564 // ----------------------------------------------------------------------
565
566#ifdef ENABLE_SCRIPTING
567 // Call the exit() function from all plugins
568 extensions().executeExitActions();
569#endif
570
571 close();
572}
573
574void App::close()
575{
576#ifdef ENABLE_UI
577 if (isGui()) {
578 // Select no document
579 static_cast<UIContext*>(context())->setActiveView(nullptr);
580
581 // Delete backups (this is a normal shutdown, we are not handling
582 // exceptions, and we are not in a destructor).
583 m_modules->deleteDataRecovery();
584 }
585#endif
586}
587
588// Finishes the Aseprite application.
589App::~App()
590{
591 try {
592 LOG("APP: Exit\n");
593 ASSERT(m_instance == this);
594
595#ifdef ENABLE_SCRIPTING
596 // Destroy scripting engine calling a method (instead of using
597 // reset()) because we need to keep the "m_engine" pointer valid
598 // until the very end, just in case that some Lua error happens
599 // now and we have to print that error using
600 // App::instance()->scriptEngine() in some way. E.g. if a Dialog
601 // onclose event handler fails with a Lua error when we are
602 // closing the app, a Lua error must be printed, and we need a
603 // valid m_engine pointer.
604 m_engine->destroy();
605 m_engine.reset();
606#endif
607
608 // Delete file formats.
609 FileFormatsManager::destroyInstance();
610
611 // Fire App Exit signal.
612 App::instance()->Exit();
613
614#ifdef ENABLE_UI
615 // Finalize modules, configuration and core.
616 Editor::destroyEditorSharedInternals();
617
618 m_backupIndicator.reset();
619
620 // Save brushes
621 m_brushes.reset();
622#endif
623
624 m_legacy.reset();
625 m_modules.reset();
626
627 // Save preferences only if we are running in GUI mode. when we
628 // run in batch mode we might want to reset some preferences so
629 // the scripts have a reproducible behavior. Those reset
630 // preferences must not be saved.
631 if (isGui())
632 preferences().save();
633
634 m_coreModules.reset();
635
636#ifdef ENABLE_UI
637 // Destroy the loaded gui.xml data.
638 KeyboardShortcuts::destroyInstance();
639 GuiXml::destroyInstance();
640#endif
641 }
642 catch (const std::exception& e) {
643 LOG(ERROR, "APP: Error: %s\n", e.what());
644 os::error_message(e.what());
645
646 // no re-throw
647 }
648 catch (...) {
649 os::error_message("Error closing the program.\n(uncaught exception)");
650
651 // no re-throw
652 }
653
654 m_instance = nullptr;
655}
656
657Context* App::context()
658{
659 return &m_coreModules->m_context;
660}
661
662bool App::isPortable()
663{
664 static std::optional<bool> is_portable;
665 if (!is_portable) {
666 is_portable =
667 base::is_file(base::join_path(
668 base::get_file_path(base::get_app_path()),
669 "aseprite.ini"));
670 }
671 return *is_portable;
672}
673
674tools::ToolBox* App::toolBox() const
675{
676 ASSERT(m_modules != NULL);
677 return &m_modules->m_toolbox;
678}
679
680tools::Tool* App::activeTool() const
681{
682 return m_modules->m_activeToolManager.activeTool();
683}
684
685tools::ActiveToolManager* App::activeToolManager() const
686{
687 return &m_modules->m_activeToolManager;
688}
689
690RecentFiles* App::recentFiles() const
691{
692#ifdef ENABLE_UI
693 ASSERT(m_modules != NULL);
694 return &m_modules->m_recent_files;
695#else
696 return nullptr;
697#endif
698}
699
700Workspace* App::workspace() const
701{
702 if (m_mainWindow)
703 return m_mainWindow->getWorkspace();
704 else
705 return nullptr;
706}
707
708ContextBar* App::contextBar() const
709{
710 if (m_mainWindow)
711 return m_mainWindow->getContextBar();
712 else
713 return nullptr;
714}
715
716Timeline* App::timeline() const
717{
718 if (m_mainWindow)
719 return m_mainWindow->getTimeline();
720 else
721 return nullptr;
722}
723
724Preferences& App::preferences() const
725{
726 return m_coreModules->m_context.preferences();
727}
728
729Extensions& App::extensions() const
730{
731 return m_modules->m_extensions;
732}
733
734crash::DataRecovery* App::dataRecovery() const
735{
736 return m_modules->recovery();
737}
738
739#ifdef ENABLE_UI
740void App::showNotification(INotificationDelegate* del)
741{
742 if (m_mainWindow)
743 m_mainWindow->showNotification(del);
744}
745
746void App::showBackupNotification(bool state)
747{
748 assert_ui_thread();
749 if (state) {
750 if (!m_backupIndicator)
751 m_backupIndicator = std::make_unique<BackupIndicator>();
752 m_backupIndicator->start();
753 }
754 else {
755 if (m_backupIndicator)
756 m_backupIndicator->stop();
757 }
758}
759
760void App::updateDisplayTitleBar()
761{
762 std::string defaultTitle = fmt::format("{} v{}", get_app_name(), get_app_version());
763 std::string title;
764
765 DocView* docView = UIContext::instance()->activeView();
766 if (docView) {
767 // Prepend the document's filename.
768 title += docView->document()->name();
769 title += " - ";
770 }
771
772 title += defaultTitle;
773 os::instance()->defaultWindow()->setTitle(title);
774}
775
776InputChain& App::inputChain()
777{
778 return m_modules->m_inputChain;
779}
780#endif
781
782// Updates palette and redraw the screen.
783void app_refresh_screen()
784{
785#ifdef ENABLE_UI
786 Context* ctx = UIContext::instance();
787 ASSERT(ctx != NULL);
788
789 Site site = ctx->activeSite();
790 if (Palette* pal = site.palette())
791 set_current_palette(pal, false);
792 else
793 set_current_palette(nullptr, false);
794
795 // Invalidate the whole screen.
796 ui::Manager::getDefault()->invalidate();
797#endif // ENABLE_UI
798}
799
800// TODO remove app_rebuild_documents_tabs() and replace it by
801// observable events in the document (so a tab can observe if the
802// document is modified).
803void app_rebuild_documents_tabs()
804{
805#ifdef ENABLE_UI
806 if (App::instance()->isGui()) {
807 App::instance()->workspace()->updateTabs();
808 App::instance()->updateDisplayTitleBar();
809 }
810#endif // ENABLE_UI
811}
812
813PixelFormat app_get_current_pixel_format()
814{
815 Context* ctx = App::instance()->context();
816 ASSERT(ctx);
817
818 Doc* doc = ctx->activeDocument();
819 if (doc)
820 return doc->sprite()->pixelFormat();
821 else
822 return IMAGE_RGB;
823}
824
825int app_get_color_to_clear_layer(Layer* layer)
826{
827 ASSERT(layer != NULL);
828
829 app::Color color;
830
831 // The `Background' is erased with the `Background Color'
832 if (layer->isBackground()) {
833#ifdef ENABLE_UI
834 if (ColorBar::instance())
835 color = ColorBar::instance()->getBgColor();
836 else
837#endif
838 color = app::Color::fromRgb(0, 0, 0); // TODO get background color color from doc::Settings
839 }
840 else // All transparent layers are cleared with the mask color
841 color = app::Color::fromMask();
842
843 return color_utils::color_for_layer(color, layer);
844}
845
846#ifdef ENABLE_DRM
847void app_configure_drm() {
848 ResourceFinder userDirRf, dataDirRf;
849 userDirRf.includeUserDir("");
850 dataDirRf.includeDataDir("");
851 std::map<std::string, std::string> config = {
852 {"data", dataDirRf.getFirstOrCreateDefault()}
853 };
854 DRM_CONFIGURE(get_app_url(), get_app_name(), get_app_version(), userDirRf.getFirstOrCreateDefault(), updater::getUserAgent(), config);
855}
856#endif
857
858} // namespace app
859