Add web support, and fix a bug.
This commit is contained in:
parent
39dde288fe
commit
ee3962bda2
24 changed files with 1271 additions and 344 deletions
147
CMakeLists.txt
147
CMakeLists.txt
|
@ -1,5 +1,10 @@
|
|||
cmake_minimum_required(VERSION 3.28)
|
||||
project(looper VERSION 1.0.0 LANGUAGES C CXX)
|
||||
|
||||
if("${CMAKE_SYSTEM_NAME}" STREQUAL "Emscripten")
|
||||
set(EMSCRIPTEN ON)
|
||||
message("Building for WASM.")
|
||||
endif()
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(SDL_MIXER_X_STATIC ON CACHE BOOL "")
|
||||
|
@ -23,26 +28,82 @@ set(BUILD_WINAMP OFF CACHE BOOL "" FORCE)
|
|||
set(BUILD_XMPLAY OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_AUDACIOUS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_V123 OFF CACHE BOOL "" FORCE)
|
||||
set(JSONCPP_WITH_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_STATIC_LIBS ON CACHE BOOL "" FORCE)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(JSONCPP_WITH_PKGCONFIG_SUPPORT OFF CACHE BOOL "" FORCE)
|
||||
set(JSONCPP_WITH_CMAKE_PACKAGE OFF CACHE BOOL "" FORCE)
|
||||
option(ENABLE_DBUS "Enables DBus support" ON)
|
||||
option(USE_PORTALS "Enables libportal" ON)
|
||||
if (DEFINED EMSCRIPTEN)
|
||||
set(BUILD_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(ENABLE_DBUS OFF CACHE BOOL "" FORCE)
|
||||
set(DOWNLOAD_AUDIO_CODECS_DEPENDENCY ON CACHE BOOL "" FORCE)
|
||||
set(SDL_MIXER_X_SHARED OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_MIXER_X_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(USE_OGG_VORBIS_STB ON CACHE BOOL "" FORCE)
|
||||
set(USE_OPUS OFF CACHE BOOL "" FORCE)
|
||||
set(USE_MODPLUG OFF CACHE BOOL "" FORCE)
|
||||
set(USE_GME OFF CACHE BOOL "" FORCE)
|
||||
set(USE_WAVPACK OFF CACHE BOOL "" FORCE)
|
||||
set(USE_XMP OFF CACHE BOOL "" FORCE)
|
||||
set(USE_MIDI_EDMIDI OFF CACHE BOOL "" FORCE)
|
||||
set(USE_PORTALS OFF CACHE BOOL "" FORCE)
|
||||
set(USE_SYSTEM_SDL2 ON CACHE BOOL "" FORCE)
|
||||
set(USE_MPEG OFF CACHE BOOL "" FORCE)
|
||||
set(USE_CELT OFF CACHE BOOL "" FORCE)
|
||||
set(USE_ATRAC9 OFF CACHE BOOL "" FORCE)
|
||||
set(USE_SPEEX OFF CACHE BOOL "" FORCE)
|
||||
set(USE_G719 OFF CACHE BOOL "" FORCE)
|
||||
set(EXTRA_FLAGS "-sUSE_VORBIS -sUSE_MPG123=1 -sUSE_ZLIB -sUSE_OGG=1 -sUSE_MODPLUG=1 -sUSE_SDL=2 -sUSE_SDL_IMAGE=2 --shell-file=${CMAKE_CURRENT_SOURCE_DIR}/web/shell.html --js-library=${CMAKE_CURRENT_SOURCE_DIR}/web/api.js")
|
||||
set(DEBUG_INFO ${CMAKE_BUILD_TYPE} STREQUAL Debug OR ${CMAKE_BUILD_TYPE} STREQUAL RelWithDebInfo)
|
||||
set(RELASE_OPTS ${CMAKE_BUILD_TYPE} STREQUAL Release OR ${CMAKE_BUILD_TYPE} STREQUAL RelWithDebInfo)
|
||||
set(PROFILE_ENABLED ${CMAKE_BUILD_TYPE} STREQUAL RelWithDebInfo)
|
||||
set(EXTRA_LINKER_FLAGS "-sALLOW_MEMORY_GROWTH=1 -sEXPORTED_RUNTIME_METHODS=UTF8ToString,stringToUTF8,lengthBytesUTF8 -sEXPORTED_FUNCTIONS=_malloc,_main -sASYNCIFY_IMPORTS=read_file")
|
||||
set(OPENMP OFF CACHE BOOL "" FORCE)
|
||||
set(SOUNDSTRETCH OFF CACHE BOOL "" FORCE)
|
||||
set(SOUNDTOUCH_DLL OFF CACHE BOOL "" FORCE)
|
||||
if(DEBUG_INFO)
|
||||
option(STACK_OVERFLOW_CHECK "Enables extra stack overflow checks" OFF)
|
||||
if(${STACK_OVERFLOW_CHECK})
|
||||
set(EXTRA_LINKER_FLAGS "${EXTRA_LINKER_FLAGS} -sSTACK_OVERFLOW_CHECK=2")
|
||||
endif()
|
||||
set(EXTRA_FLAGS "${EXTRA_FLAGS} -g -gsource-map")
|
||||
set(EXTRA_LINKER_FLAGS "${EXTRA_LINKER_FLAGS} --emit-symbol-map -sASSERTIONS=1")
|
||||
endif()
|
||||
if(RELEASE_OPTS)
|
||||
set(EXTRA_FLAGS "${EXTRA_FLAGS} -flto")
|
||||
set(EXTRA_LINKER_FLAGS "${EXTRA_LINKER_FLAGS} --closure 1")
|
||||
endif()
|
||||
if(PROFILE_ENABLED)
|
||||
set(EXTRA_LINKER_FLAGS "${EXTRA_LINKER_FLAGS} --profiling --profiling-funcs")
|
||||
endif()
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${EXTRA_FLAGS}")
|
||||
set(EXTRA_LINKER_FLAGS "${EXTRA_LINKER_FLAGS} ${EXTRA_FLAGS}")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${EXTRA_LINKER_FLAGS}")
|
||||
set(CMAKE_FIND_ROOT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/web/cmake" ${CMAKE_FIND_ROOT_PATH})
|
||||
set(SDL2_DIR ${CMAKE_CURRENT_SOURCE_DIR}/web/cmake)
|
||||
else()
|
||||
set(BUILD_STATIC OFF CACHE BOOL "")
|
||||
option(USE_VGMSTREAM "Enable using the VGMStream libraries (Unimplemented)" OFF)
|
||||
|
||||
endif()
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(jsoncpp IMPORTED_TARGET jsoncpp)
|
||||
#add_subdirectory(subprojects/jsoncpp)
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(sdbus-c++ REQUIRED)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
add_subdirectory(subprojects/SDL-Mixer-X)
|
||||
add_subdirectory(subprojects/vgmstream)
|
||||
|
||||
if (DEFINED EMSCRIPTEN)
|
||||
set(EXTRA_LIBS )
|
||||
else()
|
||||
set(EXTRA_LIBS libvgmstream_shared)
|
||||
endif()
|
||||
if(SDL_MIXER_X_STATIC)
|
||||
set(SDL_MIXER_X_TARGET SDL2_mixer_ext_Static)
|
||||
else()
|
||||
set(SDL_MIXER_X_TARGET SDL2_mixer_ext)
|
||||
set(EXTRA_LIBS ${EXTRA_LIBS} ${SDL_MIXER_X_TARGET})
|
||||
endif()
|
||||
pkg_check_modules(SoundTouch IMPORTED_TARGET soundtouch)
|
||||
add_custom_target(looper_assets COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/assets/update_assets.py WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
find_package(Git)
|
||||
if (Git_FOUND)
|
||||
|
@ -54,6 +115,7 @@ endif()
|
|||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set(DEBUG ON)
|
||||
endif()
|
||||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
|
||||
include(log)
|
||||
#execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/meson2cmake_cfg.py ${CMAKE_CURRENT_SOURCE_DIR}/config.meson.h.in ${CMAKE_CURRENT_SOURCE_DIR}/config.cmake.h.in)
|
||||
|
@ -91,7 +153,6 @@ macro(target_pkgconfig)
|
|||
pop_fnstack()
|
||||
endmacro()
|
||||
set(INC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} subprojects/vgmstream/src subprojects/vgmstream/src/base)
|
||||
option(USE_PORTALS "Enable libportal support if available" ON)
|
||||
set(UI_BACKENDS "")
|
||||
list(POP_FRONT UI_BACKENDS)
|
||||
macro(prefix_all)
|
||||
|
@ -113,7 +174,22 @@ prefix_all(LIBRARY_SOURCES
|
|||
)
|
||||
add_library(liblooper STATIC ${LIBRARY_SOURCES})
|
||||
target_include_directories(liblooper PUBLIC ${INC})
|
||||
target_link_libraries(liblooper PUBLIC SDL2::SDL2 ${SDL_MIXER_X_TARGET} PkgConfig::SoundTouch SDBusCpp::sdbus-c++ libvgmstream libvgmstream_shared)
|
||||
if(DEFINED EMSCRIPTEN)
|
||||
add_subdirectory(subprojects/jsoncpp)
|
||||
add_subdirectory(subprojects/soundtouch)
|
||||
target_link_libraries(liblooper PUBLIC ${SDL_MIXER_X_TARGET} SoundTouch libvgmstream jsoncpp_static)
|
||||
target_compile_options(liblooper PUBLIC "-sUSE_SDL=2")
|
||||
else()
|
||||
pkg_check_modules(SoundTouch IMPORTED_TARGET soundtouch)
|
||||
pkg_check_modules(jsoncpp IMPORTED_TARGET jsoncpp)
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(sdbus-c++ REQUIRED)
|
||||
target_link_libraries(liblooper PUBLIC SDL2::SDL2 ${SDL_MIXER_X_TARGET} PkgConfig::SoundTouch libvgmstream libvgmstream_shared PkgConfig::jsoncpp)
|
||||
endif()
|
||||
if (${ENABLE_DBUS})
|
||||
target_link_libraries(liblooper PUBLIC SDBusCpp::sdbus-c++)
|
||||
target_compile_definitions(liblooper PUBLIC DBUS_ENABLED)
|
||||
endif()
|
||||
macro(add_ui_backend)
|
||||
set(ARGS ${ARGV})
|
||||
list(POP_FRONT ARGS target)
|
||||
|
@ -134,30 +210,59 @@ macro(add_ui_backend)
|
|||
endif()
|
||||
endif()
|
||||
endmacro()
|
||||
option(DISABLE_GTK_UI "Disables the GTK+ UI" OFF)
|
||||
option(DISABLE_IMGUI_UI "Disables the Dear ImGui UI" OFF)
|
||||
set(ENABLED_UIS )
|
||||
if (NOT DISABLE_IMGUI_UI)
|
||||
add_subdirectory(backends/ui/imgui)
|
||||
list(APPEND ENABLED_UIS "imgui")
|
||||
macro(ui_backend_subdir)
|
||||
cmake_parse_arguments(UI_OPTS "" "SUBDIR;NAME;READABLE_NAME" "" ${ARGN} )
|
||||
message("Backend ${UI_OPTS_READABLE_NAME} defined...")
|
||||
set(UI_DISABLE_OPT DISABLE_${UI_OPTS_NAME}_UI)
|
||||
option(${UI_DISABLE_OPT} "Disables the ${UI_OPTS_READABLE_NAME} UI" OFF)
|
||||
if (NOT ${${UI_DISABLE_OPT}})
|
||||
cmake_path(GET UI_OPTS_SUBDIR STEM UI_OPTS_DIRNAME)
|
||||
set(BASE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${UI_OPTS_SUBDIR})
|
||||
add_subdirectory(${UI_OPTS_SUBDIR})
|
||||
list(APPEND ENABLED_UIS "${UI_OPTS_DIRNAME}")
|
||||
message("Enabled backend ${UI_OPTS_READABLE_NAME}.")
|
||||
else()
|
||||
message("Disabled backend ${UI_OPTS_READABLE_NAME}")
|
||||
endif()
|
||||
if (NOT DISABLE_GTK_UI)
|
||||
add_subdirectory(backends/ui/gtk)
|
||||
list(APPEND ENABLED_UIS "gtk")
|
||||
endmacro()
|
||||
set(ENABLED_UIS )
|
||||
ui_backend_subdir(NAME "IMGUI" READABLE_NAME "Dear ImGui" SUBDIR backends/ui/imgui)
|
||||
if (NOT DEFINED EMSCRIPTEN)
|
||||
ui_backend_subdir(NAME "GTK" READABLE_NAME "GTK4" SUBDIR backends/ui/gtk)
|
||||
endif()
|
||||
execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/gen_ui_backend_inc.py ${ENABLED_UIS})
|
||||
prefix_all(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/ backend_glue.cpp main.cpp daemon_backend.cpp proxy_backend.cpp)
|
||||
add_executable(looper ${SOURCES})
|
||||
add_dependencies(looper looper_assets ${UI_BACKENDS})
|
||||
if(DEFINED EMSCRIPTEN)
|
||||
set(CMAKE_EXECUTABLE_SUFFIX ".html")
|
||||
endif()
|
||||
|
||||
set(TARGET_NAME looper)
|
||||
if(DEFINED EMSCRIPTEN)
|
||||
set(TARGET_NAME index)
|
||||
endif()
|
||||
function(copy_to_bindir src dst)
|
||||
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/${src}" "$<TARGET_FILE_DIR:${TARGET_NAME}>/${dst}")
|
||||
endfunction()
|
||||
add_executable(${TARGET_NAME} ${SOURCES})
|
||||
add_dependencies(${TARGET_NAME} looper_assets ${UI_BACKENDS})
|
||||
|
||||
if(DEFINED EMSCRIPTEN)
|
||||
copy_to_bindir(assets/icon.svg icon.svg)
|
||||
copy_to_bindir(assets/icon.png icon.png)
|
||||
copy_to_bindir(web/shell.js shell.js)
|
||||
endif()
|
||||
find_program(ASCLI_EXE NAMES "appstreamcli" NO_CACHE)
|
||||
if(${ASCLI_EXE} STREQUAL "ASCLIEXE-NOTFOUND")
|
||||
message("Cannot verify Appstream Metadata.")
|
||||
else()
|
||||
add_test(NAME "verify appstream metadata" COMMAND ${ASCLI_EXE} validate --no-net --pedantic "assets/com.complecwaft.Looper.metainfo.xml")
|
||||
endif()
|
||||
target_link_libraries(looper PUBLIC liblooper PkgConfig::jsoncpp ${UI_BACKENDS})
|
||||
install(TARGETS looper ${EXTRA_LIBS})
|
||||
target_link_libraries(${TARGET_NAME} PUBLIC liblooper ${UI_BACKENDS})
|
||||
install(TARGETS ${TARGET_NAME} ${EXTRA_LIBS})
|
||||
if (NOT DEFINED EMSCRIPTEN)
|
||||
install(FILES assets/icon.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps/)
|
||||
install(FILES assets/com.complecwaft.Looper.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||
install(FILES assets/com.complecwaft.Looper.metainfo.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo)
|
||||
install(DIRECTORY assets/translations/ TYPE LOCALE PATTERN "*" EXCLUDE PATTERN "looper.pot")
|
||||
endif()
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm")
|
||||
set(GLES_NORMALLY_REQUIRED_FOR_ARCHITECTURE ON)
|
||||
elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "aarch64")
|
||||
|
@ -26,8 +25,6 @@ set(BACKEND_IMGUI_INC ${INC} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_
|
|||
foreach(INCDIR IN ITEMS ${BACKEND_IMGUI_INC_BASE})
|
||||
set(BACKEND_IMGUI_INC ${BACKEND_IMGUI_INC} ${CMAKE_CURRENT_SOURCE_DIR}/${INCDIR})
|
||||
endforeach()
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(SDL2_image REQUIRED)
|
||||
if(${USE_GLES})
|
||||
set(GLComponents GLES2)
|
||||
set(GLTarget GLES2)
|
||||
|
@ -40,6 +37,13 @@ add_ui_backend(imgui_ui ${BACKEND_IMGUI_SRC})
|
|||
if(${USE_GLES})
|
||||
target_compile_definitions(imgui_ui PRIVATE "IMGUI_IMPL_OPENGL_ES2")
|
||||
endif()
|
||||
if(DEFINED EMSCRIPTEN)
|
||||
target_compile_options(imgui_ui PRIVATE "-sUSE_SDL_IMAGE=2")
|
||||
target_link_options(imgui_ui PUBLIC "-sMAX_WEBGL_VERSION=2" "-sMIN_WEBGL_VERSION=2" "-sFULL_ES3")
|
||||
else()
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(SDL2_image REQUIRED)
|
||||
target_link_libraries(imgui_ui PRIVATE OpenGL::${GLTarget} SDL2::SDL2 SDL2_image::SDL2_image)
|
||||
endif()
|
||||
target_include_directories(imgui_ui PRIVATE ${BACKEND_IMGUI_INC})
|
||||
target_compile_definitions(imgui_ui PRIVATE IMGUI_USER_CONFIG="imgui_config.h")
|
|
@ -14,7 +14,119 @@
|
|||
#include "translation.h"
|
||||
#include <log.hpp>
|
||||
using std::vector;
|
||||
#ifdef __EMSCRIPTEN__
|
||||
extern "C" {
|
||||
extern void get_size(int32_t *x, int32_t *y);
|
||||
}
|
||||
#endif
|
||||
void RendererBackend::on_resize() {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
int32_t x, y;
|
||||
get_size(&x, &y);
|
||||
SDL_SetWindowSize(window, (int)x, (int)y);
|
||||
#endif
|
||||
}
|
||||
static RendererBackend *renderer_backend;
|
||||
|
||||
void RendererBackend::resize_static() {
|
||||
renderer_backend->resize_needed = true;
|
||||
}
|
||||
void main_loop() {
|
||||
renderer_backend->LoopFunction();
|
||||
#ifdef __EMSCRIPTEN__
|
||||
if (renderer_backend->done) {
|
||||
renderer_backend->BackendDeinit();
|
||||
emscripten_cancel_main_loop();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
void RendererBackend::BackendDeinit() {
|
||||
ImGuiIO& io = ImGui::GetIO(); (void)io;
|
||||
// Cleanup
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
|
||||
SDL_GL_DeleteContext(gl_context);
|
||||
SDL_DestroyWindow(window);
|
||||
IMG_Quit();
|
||||
SDL_Quit();
|
||||
free((void*)io.IniFilename);
|
||||
Deinit();
|
||||
renderer_backend = nullptr;
|
||||
}
|
||||
void RendererBackend::LoopFunction() {
|
||||
ImGuiIO& io = ImGui::GetIO(); (void)io;
|
||||
if (resize_needed) {
|
||||
on_resize();
|
||||
}
|
||||
auto next_frame = std::chrono::steady_clock::now() + std::chrono::milliseconds(1000 / framerate);
|
||||
// Poll and handle events (inputs, window resize, etc.)
|
||||
// You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
|
||||
// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data.
|
||||
// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data.
|
||||
// Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event))
|
||||
{
|
||||
ImGui_ImplSDL2_ProcessEvent(&event);
|
||||
if (event.type == SDL_QUIT)
|
||||
done = true;
|
||||
if (event.type == SDL_WINDOWEVENT) {
|
||||
if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||
window_width = event.window.data1 / scale;
|
||||
window_height = event.window.data2 / scale;
|
||||
//SDL_GetWindowSize(window, &window_width, &window_height);
|
||||
}
|
||||
if (event.window.event == SDL_WINDOWEVENT_DISPLAY_CHANGED) {
|
||||
UpdateScale();
|
||||
}
|
||||
if (event.window.event == SDL_WINDOWEVENT_CLOSE && event.window.windowID == SDL_GetWindowID(window)) {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
if (event.type == SDL_DROPFILE) {
|
||||
if (event.drop.file != NULL) {
|
||||
Drop(std::string(event.drop.file));
|
||||
free(event.drop.file);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start the Dear ImGui frame
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
// Run the GUI
|
||||
GuiFunction();
|
||||
// Rendering
|
||||
ImGui::Render();
|
||||
// Update the window size.
|
||||
glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y);
|
||||
// Clear the screen.
|
||||
glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
// Tell ImGui to render.
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
|
||||
// Update and Render additional Platform Windows
|
||||
// (Platform functions may change the current OpenGL context, so we save/restore it to make it easier to paste this code elsewhere.
|
||||
// For this specific demo app we could also call SDL_GL_MakeCurrent(window, gl_context) directly)
|
||||
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
|
||||
{
|
||||
SDL_Window* backup_current_window = SDL_GL_GetCurrentWindow();
|
||||
SDL_GLContext backup_current_context = SDL_GL_GetCurrentContext();
|
||||
ImGui::UpdatePlatformWindows();
|
||||
ImGui::RenderPlatformWindowsDefault();
|
||||
SDL_GL_MakeCurrent(backup_current_window, backup_current_context);
|
||||
}
|
||||
|
||||
// Swap the buffers, and do VSync if enabled.
|
||||
SDL_GL_SwapWindow(window);
|
||||
// If not doing VSync, wait until the next frame needs to be rendered.
|
||||
if (!vsync) {
|
||||
std::this_thread::sleep_until(next_frame);
|
||||
}
|
||||
}
|
||||
struct FontData {
|
||||
const char* data;
|
||||
const ImWchar *ranges;
|
||||
|
@ -103,6 +215,9 @@ void RendererBackend::AddFonts() {
|
|||
title = add_font(vector<FontData>({FontData {notosans_thin_compressed_data_base85, io.Fonts->GetGlyphRangesDefault()}, FontData {notosansjp_thin_compressed_data_base85, io.Fonts->GetGlyphRangesJapanese()}}), 48 * scale);
|
||||
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||
}
|
||||
static EM_BOOL resize_callback(int event_type, const EmscriptenUiEvent *event, void *userdata) {
|
||||
RendererBackend::resize_static();
|
||||
}
|
||||
int RendererBackend::Run() {
|
||||
setlocale(LC_ALL, "");
|
||||
bindtextdomain("neko_player", LOCALE_DIR);
|
||||
|
@ -168,7 +283,7 @@ int RendererBackend::Run() {
|
|||
const vector<unsigned char> icon_data = DecodeBase85(icon_compressed_data_base85);
|
||||
SDL_Surface* icon = IMG_Load_RW(SDL_RWFromConstMem(icon_data.data(), icon_data.size()), 1);
|
||||
SDL_SetWindowIcon(window, icon);
|
||||
SDL_GLContext gl_context = SDL_GL_CreateContext(window);
|
||||
gl_context = SDL_GL_CreateContext(window);
|
||||
SDL_GL_MakeCurrent(window, gl_context);
|
||||
|
||||
// Setup Dear ImGui context
|
||||
|
@ -217,102 +332,26 @@ int RendererBackend::Run() {
|
|||
"HOME"
|
||||
#endif
|
||||
);
|
||||
#ifndef __EMSCRIPTEN__
|
||||
SDL_GL_SetSwapInterval(vsync ? 1 : 0);
|
||||
#endif
|
||||
theme->Apply(accent_color);
|
||||
Init();
|
||||
SDL_ShowWindow(window);
|
||||
renderer_backend = this;
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// For an Emscripten build we are disabling file-system access, so let's not attempt to do a fopen() of the imgui.ini file.
|
||||
// You may manually call LoadIniSettingsFromMemory() to load settings from your own storage.
|
||||
io.IniFilename = nullptr;
|
||||
EMSCRIPTEN_MAINLOOP_BEGIN
|
||||
emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, 0, resize_callback);
|
||||
emscripten_set_main_loop(&main_loop, 0, 1);
|
||||
#else
|
||||
while (!done)
|
||||
{
|
||||
LoopFunction();
|
||||
}
|
||||
BackendDeinit();
|
||||
#endif
|
||||
{
|
||||
auto next_frame = std::chrono::steady_clock::now() + std::chrono::milliseconds(1000 / framerate);
|
||||
// Poll and handle events (inputs, window resize, etc.)
|
||||
// You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
|
||||
// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data.
|
||||
// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data.
|
||||
// Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event))
|
||||
{
|
||||
ImGui_ImplSDL2_ProcessEvent(&event);
|
||||
if (event.type == SDL_QUIT)
|
||||
done = true;
|
||||
if (event.type == SDL_WINDOWEVENT) {
|
||||
if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||
window_width = event.window.data1 / scale;
|
||||
window_height = event.window.data2 / scale;
|
||||
//SDL_GetWindowSize(window, &window_width, &window_height);
|
||||
}
|
||||
if (event.window.event == SDL_WINDOWEVENT_DISPLAY_CHANGED) {
|
||||
UpdateScale();
|
||||
}
|
||||
if (event.window.event == SDL_WINDOWEVENT_CLOSE && event.window.windowID == SDL_GetWindowID(window)) {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
if (event.type == SDL_DROPFILE) {
|
||||
if (event.drop.file != NULL) {
|
||||
Drop(std::string(event.drop.file));
|
||||
free(event.drop.file);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start the Dear ImGui frame
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
// Run the GUI
|
||||
GuiFunction();
|
||||
// Rendering
|
||||
ImGui::Render();
|
||||
// Update the window size.
|
||||
glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y);
|
||||
// Clear the screen.
|
||||
glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
// Tell ImGui to render.
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
|
||||
// Update and Render additional Platform Windows
|
||||
// (Platform functions may change the current OpenGL context, so we save/restore it to make it easier to paste this code elsewhere.
|
||||
// For this specific demo app we could also call SDL_GL_MakeCurrent(window, gl_context) directly)
|
||||
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
|
||||
{
|
||||
SDL_Window* backup_current_window = SDL_GL_GetCurrentWindow();
|
||||
SDL_GLContext backup_current_context = SDL_GL_GetCurrentContext();
|
||||
ImGui::UpdatePlatformWindows();
|
||||
ImGui::RenderPlatformWindowsDefault();
|
||||
SDL_GL_MakeCurrent(backup_current_window, backup_current_context);
|
||||
}
|
||||
|
||||
// Swap the buffers, and do VSync if enabled.
|
||||
SDL_GL_SwapWindow(window);
|
||||
// If not doing VSync, wait until the next frame needs to be rendered.
|
||||
if (!vsync) {
|
||||
std::this_thread::sleep_until(next_frame);
|
||||
}
|
||||
|
||||
}
|
||||
// Cleanup
|
||||
#ifdef __EMSCRIPTEN__
|
||||
EMSCRIPTEN_MAINLOOP_END;
|
||||
#endif
|
||||
// Cleanup
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplSDL2_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
|
||||
SDL_GL_DeleteContext(gl_context);
|
||||
SDL_DestroyWindow(window);
|
||||
IMG_Quit();
|
||||
SDL_Quit();
|
||||
free((void*)io.IniFilename);
|
||||
Deinit();
|
||||
return 0;
|
||||
}
|
||||
void RendererBackend::Init() {
|
||||
|
|
|
@ -8,11 +8,22 @@
|
|||
#endif
|
||||
#include <SDL.h>
|
||||
#include <SDL_video.h>
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include "emscripten_mainloop_stub.h"
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
#endif
|
||||
#include <string>
|
||||
#include "theme.h"
|
||||
static const char* NAME = "Looper";
|
||||
class RendererBackend {
|
||||
void BackendDeinit();
|
||||
void LoopFunction();
|
||||
SDL_GLContext gl_context;
|
||||
bool resize_needed = true;
|
||||
void on_resize();
|
||||
public:
|
||||
static void resize_static();
|
||||
double scale = 1.0;
|
||||
SDL_Window *window;
|
||||
int window_width = 475;
|
||||
|
@ -39,4 +50,5 @@ class RendererBackend {
|
|||
void GetWindowsize(int *w, int *h);
|
||||
RendererBackend();
|
||||
~RendererBackend();
|
||||
friend void main_loop();
|
||||
};
|
38
backends/ui/imgui/emscripten_mainloop_stub.h
Normal file
38
backends/ui/imgui/emscripten_mainloop_stub.h
Normal file
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
// What does this file solves?
|
||||
// - Since Dear ImGui 1.00 we took pride that most of our examples applications had their entire
|
||||
// main-loop inside the main() function. That's because:
|
||||
// - It makes the examples easier to read, keeping the code sequential.
|
||||
// - It permit the use of local variables, making it easier to try things and perform quick
|
||||
// changes when someone needs to quickly test something (vs having to structure the example
|
||||
// in order to pass data around). This is very important because people use those examples
|
||||
// to craft easy-to-past repro when they want to discuss features or report issues.
|
||||
// - It conveys at a glance that this is a no-BS framework, it won't take your main loop away from you.
|
||||
// - It is generally nice and elegant.
|
||||
// - However, comes Emscripten... it is a wonderful and magical tech but it requires a "main loop" function.
|
||||
// - Only some of our examples would run on Emscripten. Typically the ones rendering with GL or WGPU ones.
|
||||
// - I tried to refactor those examples but felt it was problematic that other examples didn't follow the
|
||||
// same layout. Why would the SDL+GL example be structured one way and the SGL+DX11 be structured differently?
|
||||
// Especially as we are trying hard to convey that using a Dear ImGui backend in an *existing application*
|
||||
// should requires only a few dozens lines of code, and this should be consistent and symmetrical for all backends.
|
||||
// - So the next logical step was to refactor all examples to follow that layout of using a "main loop" function.
|
||||
// This worked, but it made us lose all the nice things we had...
|
||||
|
||||
// Since only about 3 examples really need to run with Emscripten, here's our solution:
|
||||
// - Use some weird macros and capturing lambda to turn a loop in main() into a function.
|
||||
// - Hide all that crap in this file so it doesn't make our examples unusually ugly.
|
||||
// As a stance and principle of Dear ImGui development we don't use C++ headers and we don't
|
||||
// want to suggest to the newcomer that we would ever use C++ headers as this would affect
|
||||
// the initial judgment of many of our target audience.
|
||||
// - Technique is based on this idea: https://github.com/ocornut/imgui/pull/2492/
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#include <functional>
|
||||
static std::function<void()> MainLoopForEmscriptenP;
|
||||
static void MainLoopForEmscripten() { MainLoopForEmscriptenP(); }
|
||||
#define EMSCRIPTEN_MAINLOOP_BEGIN MainLoopForEmscriptenP = [&]()
|
||||
#define EMSCRIPTEN_MAINLOOP_END ; emscripten_set_main_loop(MainLoopForEmscripten, 0, true)
|
||||
#else
|
||||
#define EMSCRIPTEN_MAINLOOP_BEGIN
|
||||
#define EMSCRIPTEN_MAINLOOP_END
|
||||
#endif
|
|
@ -2,7 +2,21 @@
|
|||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <cctype>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <log.hpp>
|
||||
#ifdef __EMSCRIPTEN__
|
||||
extern "C" {
|
||||
extern void open_filepicker();
|
||||
extern void set_filter(const char *filter);
|
||||
extern const char *get_first_file();
|
||||
extern bool file_picker_cancelled();
|
||||
extern bool file_picker_confirmed();
|
||||
extern bool file_picker_closed();
|
||||
extern bool file_picker_visible();
|
||||
extern bool file_picker_loading();
|
||||
extern void clear_file_selection();
|
||||
}
|
||||
#endif
|
||||
FileBrowser::FileBrowser(bool save, ImGuiFileBrowserFlags extra_fallback_flags) {
|
||||
#ifdef PORTALS
|
||||
main_context = g_main_context_default();
|
||||
|
@ -12,11 +26,21 @@ FileBrowser::FileBrowser(bool save, ImGuiFileBrowserFlags extra_fallback_flags)
|
|||
inner_filter_type = g_variant_type_new("a(us)");
|
||||
#endif
|
||||
this->save = save;
|
||||
fallback = ImGui::FileBrowser((save ? ImGuiFileBrowserFlags_CreateNewDir|ImGuiFileBrowserFlags_EnterNewFilename : 0) + extra_fallback_flags);
|
||||
this->flags = (save ? ImGuiFileBrowserFlags_CreateNewDir|ImGuiFileBrowserFlags_EnterNewFilename : 0) | extra_fallback_flags;
|
||||
fallback = ImGui::FileBrowser(this->flags);
|
||||
}
|
||||
void FileBrowser::SetTypeFilters(string name, vector<string> filters) {
|
||||
filter_name = name;
|
||||
this->filters = filters;
|
||||
#ifdef __EMSCRIPTEN__
|
||||
std::string filterStr;
|
||||
for (auto filter : filters) {
|
||||
filterStr += ",";
|
||||
filterStr += filter;
|
||||
}
|
||||
filterStr = filterStr.substr(1);
|
||||
set_filter(filterStr.c_str());
|
||||
#endif
|
||||
#ifdef PORTALS
|
||||
if (variant != NULL) {
|
||||
g_variant_unref(variant);
|
||||
|
@ -51,6 +75,8 @@ void FileBrowser::SetPwd(path path) {
|
|||
bool FileBrowser::HasSelected() {
|
||||
#ifdef PORTALS
|
||||
return selected.has_value();
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
return file_picker_confirmed();
|
||||
#else
|
||||
return fallback.HasSelected();
|
||||
#endif
|
||||
|
@ -58,6 +84,15 @@ bool FileBrowser::HasSelected() {
|
|||
path FileBrowser::GetSelected() {
|
||||
#ifdef PORTALS
|
||||
return selected.value_or(path());
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
if (HasSelected()) {
|
||||
const char *c_file = get_first_file();
|
||||
std::string name = c_file;
|
||||
free((void*)c_file);
|
||||
return name;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
#else
|
||||
return fallback.GetSelected();
|
||||
#endif
|
||||
|
@ -78,6 +113,8 @@ void FileBrowser::Open() {
|
|||
} else {
|
||||
xdp_portal_open_file(portal, NULL, title.c_str(), variant, NULL, NULL, XDP_OPEN_FILE_FLAG_NONE, NULL, &FileBrowser::FileBrowserOpenCallback, this);
|
||||
}
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
open_filepicker();
|
||||
#else
|
||||
fallback.Open();
|
||||
#endif
|
||||
|
@ -157,6 +194,47 @@ void FileBrowser::FileBrowserSaveCallback(GObject *src, GAsyncResult *res, gpoin
|
|||
void FileBrowser::Display() {
|
||||
#ifdef PORTALS
|
||||
g_main_context_iteration(main_context, false);
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
if (file_picker_visible() || file_picker_loading()) {
|
||||
if((flags & ImGuiFileBrowserFlags_NoModal))
|
||||
{
|
||||
if (window_pos.has_value())
|
||||
ImGui::SetNextWindowPos(
|
||||
window_pos.value());
|
||||
ImGui::SetNextWindowSize(
|
||||
window_size);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (window_pos.has_value())
|
||||
ImGui::SetNextWindowPos(
|
||||
window_pos.value());
|
||||
ImGui::SetNextWindowSize(
|
||||
window_size);
|
||||
}
|
||||
if(flags & ImGuiFileBrowserFlags_NoModal)
|
||||
{
|
||||
if(!ImGui::BeginPopup(title.c_str(),
|
||||
(flags & ImGuiFileBrowserFlags_NoMove ? ImGuiWindowFlags_NoMove : 0) |
|
||||
(flags & ImGuiFileBrowserFlags_NoResize ? ImGuiWindowFlags_NoResize : 0)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if(!ImGui::BeginPopupModal(title.c_str(), nullptr,
|
||||
(flags & ImGuiFileBrowserFlags_NoMove ? ImGuiWindowFlags_NoMove : 0) |
|
||||
(flags & ImGuiFileBrowserFlags_NoResize ? ImGuiWindowFlags_NoResize : 0) |
|
||||
(flags & ImGuiFileBrowserFlags_NoTitleBar ? ImGuiWindowFlags_NoTitleBar : 0)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (file_picker_loading()) {
|
||||
ImGui::Text("Loading file(s)...");
|
||||
} else {
|
||||
ImGui::Text("Please select a file...");
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
#else
|
||||
fallback.Display();
|
||||
#endif
|
||||
|
@ -164,6 +242,9 @@ void FileBrowser::Display() {
|
|||
|
||||
void FileBrowser::ClearSelected() {
|
||||
selected = optional<path>();
|
||||
#ifdef __EMSCRIPTEN__
|
||||
clear_file_selection();
|
||||
#endif
|
||||
#ifndef PORTALS
|
||||
fallback.ClearSelected();
|
||||
#endif
|
||||
|
@ -177,6 +258,8 @@ void FileBrowser::SetTitle(string title) {
|
|||
bool FileBrowser::IsOpened() {
|
||||
#ifdef PORTALS
|
||||
return open;
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
return !file_picker_closed() || file_picker_confirmed();
|
||||
#else
|
||||
return fallback.IsOpened();
|
||||
#endif
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
#ifdef PORTALS
|
||||
#include <libportal/portal.h>
|
||||
#include <libportal/filechooser.h>
|
||||
|
@ -27,6 +30,7 @@ class FileBrowser {
|
|||
static void FileBrowserOpenCallback(GObject *src, GAsyncResult *res, gpointer data);
|
||||
static void FileBrowserSaveCallback(GObject *src, GAsyncResult *res, gpointer data);
|
||||
#endif
|
||||
ImGuiFileBrowserFlags flags;
|
||||
optional<ImVec2> window_pos;
|
||||
ImVec2 window_size;
|
||||
bool open = false;
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
#include <options.hpp>
|
||||
#include "ui_backend.hpp"
|
||||
#include "thirdparty/CLI11.hpp"
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
extern "C" {
|
||||
extern bool is_puter_enabled();
|
||||
extern void enable_puter(bool enable);
|
||||
}
|
||||
#endif
|
||||
|
||||
void MainLoop::Init() {
|
||||
#ifdef PORTALS
|
||||
|
@ -16,7 +21,9 @@ void MainLoop::Init() {
|
|||
show_demo_window = false;
|
||||
|
||||
FileBrowser fileDialog(false, ImGuiFileBrowserFlags_NoTitleBar|ImGuiFileBrowserFlags_NoMove|ImGuiFileBrowserFlags_NoResize);
|
||||
#ifndef __EMSCRIPTEN__
|
||||
fileDialog.SetPwd(path(userdir) / path("Music"));
|
||||
#endif
|
||||
fileDialog.SetWindowSize(window_width, window_height);
|
||||
//fileDialog.SetWindowPos(0, 0);
|
||||
position = 0.0;
|
||||
|
@ -91,15 +98,12 @@ void MainLoop::Init() {
|
|||
}
|
||||
}
|
||||
theme->Apply(accent_color);
|
||||
FileLoaded();
|
||||
}
|
||||
void MainLoop::Drop(std::string file) {
|
||||
LoadFile(file);
|
||||
}
|
||||
void MainLoop::GuiFunction() {
|
||||
position = playback->GetPosition();
|
||||
length = playback->GetLength();
|
||||
// Set the window title if the file changed, or playback stopped.
|
||||
if (playback->handle_signals(PlaybackSignalFileChanged|PlaybackSignalStopped)) {
|
||||
void MainLoop::FileLoaded() {
|
||||
auto file_maybe = playback->get_current_title();
|
||||
if (file_maybe.has_value()) {
|
||||
auto name = file_maybe.value();
|
||||
|
@ -109,6 +113,14 @@ void MainLoop::GuiFunction() {
|
|||
}
|
||||
streams = playback->get_streams();
|
||||
}
|
||||
void MainLoop::GuiFunction() {
|
||||
playback->LoopHook();
|
||||
position = playback->GetPosition();
|
||||
length = playback->GetLength();
|
||||
// Set the window title if the file changed, or playback stopped.
|
||||
if (playback->handle_signals(PlaybackSignalFileChanged|PlaybackSignalStarted|PlaybackSignalStopped)) {
|
||||
FileLoaded();
|
||||
}
|
||||
bool lengthKnown = length > 0.0;
|
||||
auto dockid = ImGui::DockSpaceOverViewport(nullptr, ImGuiDockNodeFlags_PassthruCentralNode|ImGuiDockNodeFlags_AutoHideTabBar);
|
||||
// 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!).
|
||||
|
@ -275,6 +287,12 @@ void MainLoop::GuiFunction() {
|
|||
if (ImGui::Button(_TRI_CTX(ICON_FK_MAGIC, "Preference | Related non-preference button", "Theme Editor"), ImVec2(ImGui::GetWindowWidth() - (ImGui::GetStyle().WindowPadding.x * 2.0f), 0))) {
|
||||
theme_editor = true;
|
||||
}
|
||||
#ifdef __EMSCRIPTEN__
|
||||
bool puterEnabled = is_puter_enabled();
|
||||
if (ImGui::Checkbox("Enable Puter API", &puterEnabled)) {
|
||||
enable_puter(puterEnabled);
|
||||
}
|
||||
#endif
|
||||
static bool override_lang = lang != DEFAULT_LANG;
|
||||
if (ImGui::Checkbox(_TR_CTX("Preference | override enable checkbox", "Override language"), &override_lang)) {
|
||||
if (!override_lang) {
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
#include "imgui/misc/cpp/imgui_stdlib.h"
|
||||
#include "translation.h"
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include "../libs/emscripten/emscripten_mainloop_stub.h"
|
||||
#include "emscripten_mainloop_stub.h"
|
||||
#endif
|
||||
#include "../../../backend.hpp"
|
||||
#include "ui_backend.hpp"
|
||||
|
@ -57,6 +57,7 @@ class MainLoop : public RendererBackend {
|
|||
public:
|
||||
Playback *playback;
|
||||
vector<std::string> args;
|
||||
void FileLoaded();
|
||||
void LoadFile(std::string file);
|
||||
void Init() override;
|
||||
void GuiFunction() override;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#ifdef DBUS_ENABLED
|
||||
#include "daemon_backend.hpp"
|
||||
#include "log.hpp"
|
||||
#include <thread>
|
||||
|
@ -27,3 +28,4 @@ int DaemonGlueBackend::run(std::vector<std::string> realArgs, int argc, char **a
|
|||
}
|
||||
return 0;
|
||||
}
|
||||
#endif
|
|
@ -1,4 +1,5 @@
|
|||
#pragma once
|
||||
#ifdef DBUS_ENABLED
|
||||
#include "backend.hpp"
|
||||
class DaemonGlueBackend : public UIBackend {
|
||||
public:
|
||||
|
@ -7,3 +8,4 @@ class DaemonGlueBackend : public UIBackend {
|
|||
std::string get_name() override;
|
||||
int run(std::vector<std::string> realArgs, int argc, char **argv) override;
|
||||
};
|
||||
#endif
|
31
dbus.cpp
31
dbus.cpp
|
@ -2,6 +2,7 @@
|
|||
#include "log.hpp"
|
||||
#include "backend.hpp"
|
||||
#include <random>
|
||||
#ifdef DBUS_ENABLED
|
||||
MprisAPI::MprisAPI(sdbus::IConnection &connection, std::string objectPath, DBusAPI *dbus_api)
|
||||
: AdaptorInterfaces(connection, std::move(objectPath))
|
||||
, dbus_api(dbus_api)
|
||||
|
@ -31,8 +32,10 @@ void MprisAPI::Seek(const int64_t &offset) {
|
|||
dbus_api->Position(value);
|
||||
}
|
||||
void MprisAPI::SetPosition(const sdbus::ObjectPath &TrackId, const int64_t &offset) {
|
||||
if (TrackId == playing_track_id) {
|
||||
Seek(offset);
|
||||
}
|
||||
}
|
||||
void MprisAPI::OpenUri(const std::string &Uri) {
|
||||
dbus_api->Start(Uri, true);
|
||||
}
|
||||
|
@ -129,6 +132,13 @@ bool MprisAPI::CanEditTracks() {
|
|||
MprisAPI::~MprisAPI() {
|
||||
unregisterAdaptor();
|
||||
}
|
||||
#endif
|
||||
#ifndef DBUS_ENABLED
|
||||
DBusAPI::DBusAPI(Playback *playback, bool daemon)
|
||||
: playback(playback) {
|
||||
|
||||
}
|
||||
#else
|
||||
DBusAPI::DBusAPI(Playback *playback, sdbus::IConnection &connection, std::string objectPath, bool daemon)
|
||||
: AdaptorInterfaces(connection, std::move(objectPath))
|
||||
, daemon(daemon)
|
||||
|
@ -148,13 +158,19 @@ DBusAPI::DBusAPI(Playback *playback, sdbus::IConnection &connection, std::string
|
|||
});
|
||||
connection.enterEventLoopAsync();
|
||||
}
|
||||
#endif
|
||||
const char *DBusAPI::objectPath = "/com/complecwaft/Looper";
|
||||
const char *DBusAPI::busName = "com.complecwaft.Looper";
|
||||
DBusAPI *DBusAPI::Create(Playback *playback, bool daemon) {
|
||||
#ifdef DBUS_ENABLED
|
||||
auto connection = sdbus::createSessionBusConnection(busName);
|
||||
auto &con_ref = *connection.release();
|
||||
return new DBusAPI(playback, con_ref, objectPath, daemon);
|
||||
#else
|
||||
return new DBusAPI(playback, daemon);
|
||||
#endif
|
||||
}
|
||||
#ifdef DBUS_ENABLED
|
||||
double DBusAPI::Position() {
|
||||
return playback->GetPosition();
|
||||
}
|
||||
|
@ -417,12 +433,16 @@ std::vector<sdbus::Struct<double, std::string, int32_t>> DBusAPI::GetStreams() {
|
|||
void DBusAPI::PlayStream(const uint32_t &idx) {
|
||||
playback->play_stream((int)idx);
|
||||
}
|
||||
#endif
|
||||
DBusAPI::~DBusAPI() {
|
||||
#ifdef DBUS_ENABLED
|
||||
threadExitFlag.store(true);
|
||||
threadFunc.join();
|
||||
unregisterAdaptor();
|
||||
#endif
|
||||
}
|
||||
bool DBusAPISender::isOnlyInstance() {
|
||||
#ifdef DBUS_ENABLED
|
||||
bool output;
|
||||
try {
|
||||
auto *tmp = Create();
|
||||
|
@ -432,7 +452,11 @@ bool DBusAPISender::isOnlyInstance() {
|
|||
} catch (sdbus::Error) {
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
#ifdef DBUS_ENABLED
|
||||
|
||||
std::optional<std::string> DBusAPISender::get_current_file() {
|
||||
if (IsStopped()) {
|
||||
|
@ -544,7 +568,9 @@ float DBusAPISender::GetVolume() {
|
|||
void DBusAPISender::Update() {
|
||||
|
||||
}
|
||||
#endif
|
||||
DBusAPISender *DBusAPISender::Create() {
|
||||
#ifdef DBUS_ENABLED
|
||||
try {
|
||||
auto connection = sdbus::createSessionBusConnection();
|
||||
auto &con_ref = *connection.release();
|
||||
|
@ -554,7 +580,11 @@ DBusAPISender *DBusAPISender::Create() {
|
|||
DEBUG.writefln("sdbus::Error: %s: %s", error.getName().c_str(), error.getMessage().c_str());
|
||||
return nullptr;
|
||||
}
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
#ifdef DBUS_ENABLED
|
||||
DBusAPISender::DBusAPISender(sdbus::IConnection &connection, std::string busName, std::string objectPath)
|
||||
: ProxyInterfaces(connection, std::move(busName), std::move(objectPath)) {
|
||||
registerProxy();
|
||||
|
@ -584,3 +614,4 @@ DBusAPISender::~DBusAPISender() {
|
|||
ClearHandle(handle);
|
||||
unregisterProxy();
|
||||
}
|
||||
#endif
|
35
dbus.hpp
35
dbus.hpp
|
@ -1,14 +1,17 @@
|
|||
#pragma once
|
||||
#ifdef DBUS_ENABLED
|
||||
#include <sdbus-c++/sdbus-c++.h>
|
||||
#include <sdbus-c++/StandardInterfaces.h>
|
||||
#include "assets/dbus_stub_adaptor.hpp"
|
||||
#include "assets/dbus_stub_proxy.hpp"
|
||||
#include "assets/mpris_stub_adaptor.hpp"
|
||||
#endif
|
||||
#include "playback.h"
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <thread>
|
||||
#ifdef DBUS_ENABLED
|
||||
class DBusAPI;
|
||||
class MprisAPI : public sdbus::AdaptorInterfaces<org::mpris::MediaPlayer2_adaptor, org::mpris::MediaPlayer2::Player_adaptor, org::mpris::MediaPlayer2::TrackList_adaptor, sdbus::Properties_adaptor> {
|
||||
friend class DBusAPI;
|
||||
|
@ -99,19 +102,27 @@ class MprisAPI : public sdbus::AdaptorInterfaces<org::mpris::MediaPlayer2_adapto
|
|||
MprisAPI(sdbus::IConnection &connection, std::string objectPath, DBusAPI *dbus_api);
|
||||
~MprisAPI();
|
||||
};
|
||||
class DBusAPI : public sdbus::AdaptorInterfaces<com::complecwaft::Looper_adaptor, com::complecwaft::Looper::Errors_adaptor, org::freedesktop::Application_adaptor> {
|
||||
#endif
|
||||
class DBusAPI
|
||||
#ifdef DBUS_ENABLED
|
||||
: public sdbus::AdaptorInterfaces<com::complecwaft::Looper_adaptor, com::complecwaft::Looper::Errors_adaptor, org::freedesktop::Application_adaptor>
|
||||
#endif
|
||||
{
|
||||
std::map<std::string, void*> handles;
|
||||
size_t handle_idx = 0;
|
||||
public:
|
||||
static const char *objectPath;
|
||||
static const char *busName;
|
||||
#ifdef DBUS_ENABLED
|
||||
private:
|
||||
MprisAPI *mpris;
|
||||
sdbus::IConnection &connection;
|
||||
std::minstd_rand rand_engine;
|
||||
std::deque<std::string> *get_errors_by_handle(const std::string &handle);
|
||||
bool daemon;
|
||||
std::atomic_bool threadExitFlag = false;
|
||||
std::thread threadFunc;
|
||||
sdbus::IConnection &connection;
|
||||
public:
|
||||
static const char *objectPath;
|
||||
static const char *busName;
|
||||
void Activate(const std::map<std::string, sdbus::Variant>& platform_data) override;
|
||||
void Open(const std::vector<std::string>& uris, const std::map<std::string, sdbus::Variant>& platform_data) override;
|
||||
void ActivateAction(const std::string& action_name, const std::vector<sdbus::Variant>& parameter, const std::map<std::string, sdbus::Variant>& platform_data) override;
|
||||
|
@ -164,14 +175,23 @@ class DBusAPI : public sdbus::AdaptorInterfaces<com::complecwaft::Looper_adaptor
|
|||
std::vector<sdbus::Struct<double, std::string, int32_t>> GetStreams() override;
|
||||
void PlayStream(const uint32_t &idx) override;
|
||||
|
||||
#endif
|
||||
public:
|
||||
// API
|
||||
Playback *playback;
|
||||
#ifdef DBUS_ENABLED
|
||||
void Update();
|
||||
DBusAPI(Playback *playback, sdbus::IConnection &connection, std::string objectPath, bool daemon);
|
||||
#endif
|
||||
DBusAPI(Playback *playback, bool daemon);
|
||||
~DBusAPI();
|
||||
static DBusAPI *Create(Playback *playback, bool daemon = false);
|
||||
};
|
||||
class DBusAPISender : public Playback, public sdbus::ProxyInterfaces<com::complecwaft::Looper_proxy, com::complecwaft::Looper::Errors_proxy, org::freedesktop::Application_proxy, sdbus::Peer_proxy> {
|
||||
class DBusAPISender : public Playback
|
||||
#ifdef DBUS_ENABLED
|
||||
, public sdbus::ProxyInterfaces<com::complecwaft::Looper_proxy, com::complecwaft::Looper::Errors_proxy, org::freedesktop::Application_proxy, sdbus::Peer_proxy>
|
||||
#endif
|
||||
{
|
||||
// Cache
|
||||
double length, pitch, speed, tempo, volume;
|
||||
bool stopped, paused;
|
||||
|
@ -191,6 +211,7 @@ class DBusAPISender : public Playback, public sdbus::ProxyInterfaces<com::comple
|
|||
/// @returns A proxy to the main instance of the playback engine, or nullptr if there is none.
|
||||
static DBusAPISender *Create();
|
||||
|
||||
#ifdef DBUS_ENABLED
|
||||
// Signals. Protected so that they aren't seen as a proper API
|
||||
protected:
|
||||
void onPlaybackEngineStarted() override;
|
||||
|
@ -236,4 +257,8 @@ class DBusAPISender : public Playback, public sdbus::ProxyInterfaces<com::comple
|
|||
DBusAPISender(sdbus::IConnection &connection, std::string busName, std::string objectPath);
|
||||
public:
|
||||
~DBusAPISender();
|
||||
#else
|
||||
public:
|
||||
~DBusAPISender() = default;
|
||||
#endif
|
||||
};
|
27
main.cpp
27
main.cpp
|
@ -10,6 +10,11 @@
|
|||
using namespace Looper;
|
||||
using namespace Looper::Options;
|
||||
using namespace Looper::Log;
|
||||
#ifdef __EMSCRIPTEN__
|
||||
extern "C" {
|
||||
void quit();
|
||||
}
|
||||
#endif
|
||||
std::unordered_set<LicenseData> license_data;
|
||||
std::unordered_set<LicenseData> &get_license_data() {
|
||||
return license_data;
|
||||
|
@ -25,15 +30,17 @@ int main(int argc, char **argv) {
|
|||
int log_level;
|
||||
std::string ui_backend_option = "";
|
||||
bool full_help = false;
|
||||
bool daemonize = false;
|
||||
bool disable_gui = false;
|
||||
bool quit = false;
|
||||
bool open_window = false;
|
||||
app.add_option("-l, --log-level", LogStream::log_level, "Sets the minimum log level to display in the logs.");
|
||||
app.add_option("-u, --ui-backend", ui_backend_option, "Specifies which UI backend to use.");
|
||||
#ifdef DBUS_ENABLED
|
||||
bool daemonize = false;
|
||||
bool disable_gui = false;
|
||||
bool quit = false;
|
||||
app.add_flag("-d, --daemon", daemonize, "Daemonizes the program.");
|
||||
app.add_flag("-n, --no-gui", disable_gui, "Don't open the GUI when there is a daemon and there are settings or commands for it. Ignored in daemon mode, or when no changes in state are issued.");
|
||||
app.add_flag("-q, --quit", quit, "Quits an existing instance.");
|
||||
#endif
|
||||
try {
|
||||
app.parse(args);
|
||||
} catch (const CLI::ParseError &e) {
|
||||
|
@ -43,15 +50,18 @@ int main(int argc, char **argv) {
|
|||
exit(app.exit(e));
|
||||
}
|
||||
}
|
||||
#ifdef DBUS_ENABLED
|
||||
if (daemonize) {
|
||||
ui_backend_option = "daemon";
|
||||
}
|
||||
#endif
|
||||
args.clear();
|
||||
args = app.remaining(false);
|
||||
int new_argc = args.size();
|
||||
char **new_argv = (char**)malloc(new_argc * sizeof(char*));
|
||||
init_logging();
|
||||
|
||||
#ifdef DBUS_ENABLED
|
||||
if (quit) {
|
||||
DBusAPISender *sender = DBusAPISender::Create();
|
||||
if (sender != nullptr) {
|
||||
|
@ -62,6 +72,7 @@ int main(int argc, char **argv) {
|
|||
}
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
{
|
||||
auto looper_mit = LicenseData("Looper (MIT)", "MIT");
|
||||
auto looper_gpl = LicenseData("Looper (GPL)", "GPL-3.0-or-later");
|
||||
|
@ -96,6 +107,7 @@ int main(int argc, char **argv) {
|
|||
}
|
||||
DEBUG.writeln("Initializing frontends...");
|
||||
init_backends();
|
||||
#ifdef DBUS_ENABLED
|
||||
ProxyGlueBackend *proxy_backend = nullptr;
|
||||
if ((disable_gui && !daemonize) || quit) {
|
||||
if (!DBusAPISender::isOnlyInstance()) {
|
||||
|
@ -111,6 +123,7 @@ int main(int argc, char **argv) {
|
|||
if (daemonize) {
|
||||
UIBackend::register_backend<DaemonGlueBackend>();
|
||||
}
|
||||
#endif
|
||||
for (auto kv : UIBackend::backends) {
|
||||
kv.second->add_licenses();
|
||||
}
|
||||
|
@ -134,6 +147,7 @@ int main(int argc, char **argv) {
|
|||
args.push_back("--help");
|
||||
}
|
||||
try {
|
||||
#ifdef DBUS_ENABLED
|
||||
if (proxy_backend != nullptr && !quit) {
|
||||
if (!proxy_backend->run(args, new_argc, new_argv)) {
|
||||
throw 0;
|
||||
|
@ -143,11 +157,14 @@ int main(int argc, char **argv) {
|
|||
if (!quit) {
|
||||
UIBackend::unregister_backend<ProxyGlueBackend>();
|
||||
}
|
||||
#endif
|
||||
output = backend->run(args, new_argc, new_argv);
|
||||
#ifdef DBUS_ENABLED
|
||||
if (quit && proxy_backend != nullptr) {
|
||||
proxy_backend->quitDaemon();
|
||||
proxy_backend->unregister_self();
|
||||
}
|
||||
#endif
|
||||
} catch (int return_code) {
|
||||
if (full_help) {
|
||||
std::string helpstr = app.help();
|
||||
|
@ -168,5 +185,9 @@ int main(int argc, char **argv) {
|
|||
}
|
||||
free(new_argv);
|
||||
save_options();
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
quit();
|
||||
#endif
|
||||
return output;
|
||||
}
|
||||
|
|
80
playback.cpp
80
playback.cpp
|
@ -17,6 +17,8 @@ extern "C" {
|
|||
#include "log.hpp"
|
||||
#include <filesystem>
|
||||
#include "dbus.hpp"
|
||||
#include <format>
|
||||
#include "util.hpp"
|
||||
using namespace std::chrono;
|
||||
|
||||
int NotSDL_ConvertAudioSamples(const SDL_AudioSpec *src_spec, const Uint8 *src_data, int src_len,
|
||||
|
@ -183,10 +185,10 @@ VGMSTREAM *PlaybackInstance::LoadVgm(const char *file, int idx) {
|
|||
close_vgmstream(output);
|
||||
stream_list_mutex.lock();
|
||||
streams.clear();
|
||||
PlaybackStream defaultStream;
|
||||
defaultStream.id = 0;
|
||||
defaultStream.name = "Default";
|
||||
streams.push_back(defaultStream);
|
||||
//PlaybackStream defaultStream;
|
||||
//defaultStream.id = 0;
|
||||
//defaultStream.name = "Default";
|
||||
//streams.push_back(defaultStream);
|
||||
for (int i = 0; i <= stream_count; i++) {
|
||||
PlaybackStream stream;
|
||||
stream.id = i;
|
||||
|
@ -208,12 +210,17 @@ VGMSTREAM *PlaybackInstance::LoadVgm(const char *file, int idx) {
|
|||
char *buf = (char*)malloc(STREAM_NAME_SIZE + 1);
|
||||
memset(buf, 0, STREAM_NAME_SIZE + 1);
|
||||
strncpy(buf, tmp->stream_name, STREAM_NAME_SIZE);
|
||||
|
||||
if (buf[0] == '\0') {
|
||||
free(buf);
|
||||
buf = strdup("Unknown");
|
||||
}
|
||||
if (i == 0) {
|
||||
stream.name = std::format("Default ({})", buf);
|
||||
} else {
|
||||
stream.name = buf;
|
||||
}
|
||||
DEBUG.writefln("Stream %d: '%s' (Length: %s)", stream.id, stream.name.c_str(), TimeToString(stream.length).c_str());
|
||||
streams.push_back(stream);
|
||||
free(buf);
|
||||
close_vgmstream(tmp);
|
||||
}
|
||||
|
@ -298,15 +305,11 @@ void PlaybackInstance::UpdateST() {
|
|||
double PlaybackInstance::GetMaxSeconds() {
|
||||
return std::max((double)(MaxSpeed * MaxTempo), st->getInputOutputSampleRatio());
|
||||
}
|
||||
void PlaybackInstance::ThreadFunc() {
|
||||
#ifdef __linux__
|
||||
pthread_setname_np(pthread_self(), "Playback control thread");
|
||||
#endif
|
||||
void PlaybackInstance::InitLoopFunction() {
|
||||
bool reload = false;
|
||||
speed_changed.store(true);
|
||||
tempo_changed.store(true);
|
||||
pitch_changed.store(true);
|
||||
while (running) {
|
||||
playback_ready.store(false);
|
||||
if (!SDL_WasInit(SDL_INIT_AUDIO)) {
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
|
||||
|
@ -334,7 +337,8 @@ void PlaybackInstance::ThreadFunc() {
|
|||
ERROR.writefln("Error opening audio device: '%s'", SDL_GetError());
|
||||
set_error("Failed to open audio device!");
|
||||
running = false;
|
||||
break;
|
||||
loop_started = false;
|
||||
return;
|
||||
}
|
||||
spec = obtained;
|
||||
st->setSampleRate(spec.freq);
|
||||
|
@ -354,16 +358,17 @@ void PlaybackInstance::ThreadFunc() {
|
|||
set_error("Failed to allocate memory for playback!");
|
||||
set_signal(PlaybackSignalErrorOccurred);
|
||||
running = false;
|
||||
break;
|
||||
loop_started = false;
|
||||
return;
|
||||
}
|
||||
bufsize = new_bufsize;
|
||||
general_mixer = Mix_GetGeneralMixer();
|
||||
Mix_InitMixer(&fakespec, SDL_FALSE);
|
||||
SDL_PauseAudioDevice(device, 0);
|
||||
stream = LoadVgm(filePath.c_str(), 0);
|
||||
Mix_Music *music = nullptr;
|
||||
if (stream == nullptr) {
|
||||
music = LoadMix(filePath.c_str());
|
||||
stream = nullptr;
|
||||
if (music == nullptr) {
|
||||
stream = LoadVgm(filePath.c_str(), 0);
|
||||
}
|
||||
|
||||
reload = false;
|
||||
|
@ -373,7 +378,9 @@ void PlaybackInstance::ThreadFunc() {
|
|||
playback_ready.store(false);
|
||||
}
|
||||
set_signal(PlaybackSignalStarted);
|
||||
while (running) {
|
||||
}
|
||||
void PlaybackInstance::LoopFunction() {
|
||||
|
||||
if (file_changed.exchange(false) || load_requested.exchange(false)) {
|
||||
if (stream != nullptr) {
|
||||
UnloadVgm(stream);
|
||||
|
@ -381,9 +388,10 @@ void PlaybackInstance::ThreadFunc() {
|
|||
if (music != nullptr) {
|
||||
UnloadMix(music);
|
||||
}
|
||||
stream = LoadVgm(filePath.c_str(), 0);
|
||||
if (stream == nullptr) {
|
||||
music = LoadMix(filePath.c_str());
|
||||
stream = nullptr;
|
||||
if (music == nullptr) {
|
||||
stream = LoadVgm(filePath.c_str(), 0);
|
||||
}
|
||||
if (music || stream) {
|
||||
playback_ready.store(true);
|
||||
|
@ -469,7 +477,8 @@ void PlaybackInstance::ThreadFunc() {
|
|||
set_error("Failed to allocate memory for playback!");
|
||||
set_signal(PlaybackSignalErrorOccurred);
|
||||
running = false;
|
||||
break;
|
||||
stop_loop();
|
||||
return;
|
||||
}
|
||||
bufsize = correct_buf_size;
|
||||
}
|
||||
|
@ -486,8 +495,9 @@ void PlaybackInstance::ThreadFunc() {
|
|||
} else if (music != nullptr) {
|
||||
position = Mix_GetMusicPosition(music);
|
||||
}
|
||||
std::this_thread::sleep_for(20ms);
|
||||
|
||||
}
|
||||
void PlaybackInstance::DeinitLoopFunction() {
|
||||
playback_ready.store(false);
|
||||
// ====
|
||||
if (music != nullptr) {
|
||||
|
@ -502,12 +512,22 @@ void PlaybackInstance::ThreadFunc() {
|
|||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
delete st;
|
||||
free(buf);
|
||||
}
|
||||
current_file_mutex.lock();
|
||||
current_file = {};
|
||||
current_file_mutex.unlock();
|
||||
set_signal(PlaybackSignalStopped);
|
||||
}
|
||||
void PlaybackInstance::ThreadFunc() {
|
||||
#ifdef __linux__
|
||||
pthread_setname_np(pthread_self(), "Playback control thread");
|
||||
#endif
|
||||
start_loop();
|
||||
while (running && loop_started) {
|
||||
LoopHook();
|
||||
std::this_thread::sleep_for(20ms);
|
||||
}
|
||||
stop_loop();
|
||||
}
|
||||
|
||||
PlaybackInstance::PlaybackInstance() {
|
||||
running = false;
|
||||
|
@ -546,7 +566,11 @@ void PlaybackInstance::Load(std::string filePath) {
|
|||
if (running.exchange(true)) {
|
||||
load_requested.store(true);
|
||||
} else {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
start_loop();
|
||||
#else
|
||||
thread = std::thread(&PlaybackInstance::ThreadFunc, this);
|
||||
#endif
|
||||
}
|
||||
flag_mutex.lock();
|
||||
this->position = 0.0;
|
||||
|
@ -557,9 +581,15 @@ void PlaybackInstance::Load(std::string filePath) {
|
|||
}
|
||||
void PlaybackInstance::Start(std::string filePath, int streamIdx) {
|
||||
Load(filePath);
|
||||
while (!load_finished.exchange(false)) {
|
||||
while (loop_started && !load_finished.exchange(false)) {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
LoopHook();
|
||||
#endif
|
||||
std::this_thread::sleep_for(20ms);
|
||||
}
|
||||
if (!loop_started) {
|
||||
return;
|
||||
}
|
||||
INFO.writefln("Playing %s...", filePath.c_str());
|
||||
flag_mutex.lock();
|
||||
this->position = 0.0;
|
||||
|
@ -607,7 +637,11 @@ bool PlaybackInstance::IsPaused() {
|
|||
|
||||
void PlaybackInstance::Stop() {
|
||||
if (running.exchange(false)) {
|
||||
#ifdef __EMSCRIPTEN__
|
||||
stop_loop();
|
||||
#else
|
||||
thread.join();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
void PlaybackInstance::Update() {
|
||||
|
@ -713,6 +747,7 @@ void Playback::set_error(std::string desc) {
|
|||
set_signal(PlaybackSignalErrorOccurred);
|
||||
}
|
||||
Playback *Playback::Create(bool *daemon_found, bool daemon) {
|
||||
#ifdef DBUS_ENABLED
|
||||
auto *dbus_proxy = DBusAPISender::Create();
|
||||
if (dbus_proxy != nullptr) {
|
||||
if (daemon_found != nullptr) {
|
||||
|
@ -729,6 +764,7 @@ Playback *Playback::Create(bool *daemon_found, bool daemon) {
|
|||
if (daemon_found != nullptr) {
|
||||
*daemon_found = false;
|
||||
}
|
||||
#endif
|
||||
DEBUG.writeln("Creating new playback instance.");
|
||||
return new PlaybackInstance();
|
||||
}
|
||||
|
|
29
playback.h
29
playback.h
|
@ -185,6 +185,30 @@ class Playback {
|
|||
Pause();
|
||||
}
|
||||
}
|
||||
virtual void InitLoopFunction() {
|
||||
|
||||
}
|
||||
virtual void LoopFunction() {
|
||||
|
||||
}
|
||||
virtual void DeinitLoopFunction() {
|
||||
|
||||
}
|
||||
bool loop_started = false;
|
||||
virtual void start_loop() {
|
||||
InitLoopFunction();
|
||||
loop_started = true;
|
||||
}
|
||||
virtual void stop_loop() {
|
||||
DeinitLoopFunction();
|
||||
loop_started = false;
|
||||
}
|
||||
virtual void LoopHook() {
|
||||
if (loop_started) {
|
||||
LoopFunction();
|
||||
}
|
||||
}
|
||||
|
||||
static Playback *Create(bool *daemon_found, bool daemon = false);
|
||||
};
|
||||
class DBusAPISender;
|
||||
|
@ -227,6 +251,7 @@ private:
|
|||
void UnloadMix(Mix_Music* music);
|
||||
void UnloadVgm(VGMSTREAM *stream);
|
||||
VGMSTREAM *stream;
|
||||
Mix_Music *music;
|
||||
std::vector<PlaybackStream> streams;
|
||||
std::mutex stream_list_mutex;
|
||||
double real_volume = 1.0;
|
||||
|
@ -239,7 +264,6 @@ private:
|
|||
std::optional<std::string> current_file;
|
||||
std::optional<std::string> current_title;
|
||||
float prev_pitch, prev_speed, prev_tempo;
|
||||
|
||||
public:
|
||||
PlaybackInstance();
|
||||
~PlaybackInstance() override;
|
||||
|
@ -269,6 +293,9 @@ public:
|
|||
float GetPitch() override;
|
||||
float GetSpeed() override;
|
||||
float GetVolume() override;
|
||||
void InitLoopFunction() override;
|
||||
void DeinitLoopFunction() override;
|
||||
void LoopFunction() override;
|
||||
float volume;
|
||||
float speed;
|
||||
float tempo;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#ifdef DBUS_ENABLED
|
||||
#include "proxy_backend.hpp"
|
||||
#include "log.hpp"
|
||||
#include <thread>
|
||||
|
@ -30,3 +31,4 @@ int ProxyGlueBackend::run(std::vector<std::string> realArgs, int argc, char **ar
|
|||
void ProxyGlueBackend::quitDaemon() {
|
||||
((DBusAPISender*)playback)->Quit();
|
||||
}
|
||||
#endif
|
|
@ -1,4 +1,5 @@
|
|||
#pragma once
|
||||
#ifdef DBUS_ENABLED
|
||||
#include "backend.hpp"
|
||||
class ProxyGlueBackend : public UIBackend {
|
||||
DBusAPISender *sender;
|
||||
|
@ -9,3 +10,4 @@ class ProxyGlueBackend : public UIBackend {
|
|||
void quitDaemon();
|
||||
int run(std::vector<std::string> realArgs, int argc, char **argv) override;
|
||||
};
|
||||
#endif
|
1
subprojects/soundtouch
Submodule
1
subprojects/soundtouch
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit e83424d5928ab8513d2d082779c275765dee31b9
|
55
web/api.js
Normal file
55
web/api.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
|
||||
addToLibrary({
|
||||
open_filepicker: function() {
|
||||
window.filePicker.show();
|
||||
},
|
||||
set_filter: function(filter) {
|
||||
window.filePicker.setFilter(Module.UTF8ToString(filter));
|
||||
},
|
||||
file_picker_confirmed: function() {
|
||||
return window.filePicker.wasConfirmed();
|
||||
},
|
||||
file_picker_closed: function() {
|
||||
return window.filePicker.wasClosed();
|
||||
},
|
||||
file_picker_cancelled: function() {
|
||||
return window.filePicker.wasCancelled();
|
||||
},
|
||||
get_first_file: function() {
|
||||
if (window.filePicker.wasConfirmed()) {
|
||||
let output = window.filePicker.getFirstFile();
|
||||
let len = Module.lengthBytesUTF8(output) + 1;
|
||||
let outptr = Module._malloc(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
setValue(outptr + i, 0, 'i8');
|
||||
}
|
||||
Module.stringToUTF8(output, outptr, len);
|
||||
return outptr;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
file_picker_visible: function() {
|
||||
return window.filePicker.visible;
|
||||
},
|
||||
file_picker_loading: function() {
|
||||
return window.filePicker.loading;
|
||||
},
|
||||
clear_file_selection: function() {
|
||||
window.filePicker.clearSelection();
|
||||
},
|
||||
get_size: function(x, y) {
|
||||
let canvas = document.getElementById("canvas");
|
||||
setValue(x, canvas.offsetWidth, "i32");
|
||||
setValue(y, canvas.offsetHeight, "i32");
|
||||
},
|
||||
is_puter_enabled: function() {
|
||||
return window.filePicker.puterEnabled;
|
||||
},
|
||||
enable_puter: function(enable) {
|
||||
window.filePicker.puterEnabled = enable;
|
||||
},
|
||||
quit: function() {
|
||||
puter.ui.exit();
|
||||
}
|
||||
})
|
30
web/cmake/SDL2Config.cmake
Normal file
30
web/cmake/SDL2Config.cmake
Normal file
|
@ -0,0 +1,30 @@
|
|||
# sdl2 cmake project-config input for CMakeLists.txt script
|
||||
|
||||
include(FeatureSummary)
|
||||
set_package_properties(SDL2 PROPERTIES
|
||||
URL "https://www.libsdl.org/"
|
||||
DESCRIPTION "low level access to audio, keyboard, mouse, joystick, and graphics hardware"
|
||||
)
|
||||
|
||||
########################################################################
|
||||
|
||||
set(SDL2_FOUND TRUE)
|
||||
|
||||
set(SDL2_SDL2_FOUND TRUE)
|
||||
set(SDL2_SDL2-static_FOUND TRUE)
|
||||
set(SDL2_SDL2main_FOUND TRUE)
|
||||
set(SDL2_SDL2test_FOUND TRUE)
|
||||
|
||||
|
||||
|
||||
add_library(SDL2::SDL2 INTERFACE IMPORTED)
|
||||
target_link_options(SDL2::SDL2 INTERFACE "-sUSE_SDL=2")
|
||||
target_compile_options(SDL2::SDL2 INTERFACE "-sUSE_SDL=2")
|
||||
add_library(SDL2::SDL2-static INTERFACE IMPORTED)
|
||||
target_link_options(SDL2::SDL2-static INTERFACE "-sUSE_SDL=2")
|
||||
target_compile_options(SDL2::SDL2-static INTERFACE "-sUSE_SDL=2")
|
||||
set(SDL2_LIBRARIES SDL2::SDL2)
|
||||
set(SDL2_STATIC_LIBRARIES SDL2::SDL2-static)
|
||||
set(SDL2_STATIC_PRIVATE_LIBS)
|
||||
|
||||
set(SDL2MAIN_LIBRARY)
|
48
web/cmake/SDL2_imageConfig.cmake
Normal file
48
web/cmake/SDL2_imageConfig.cmake
Normal file
|
@ -0,0 +1,48 @@
|
|||
# sdl2 cmake project-config input for CMakeLists.txt script
|
||||
|
||||
include(FeatureSummary)
|
||||
set_package_properties(SDL2_image PROPERTIES
|
||||
URL "https://www.libsdl.org/projects/SDL_image/"
|
||||
DESCRIPTION "SDL_image is an image file loading library"
|
||||
)
|
||||
|
||||
########################################################################
|
||||
|
||||
set(SDL2_image_FOUND TRUE)
|
||||
|
||||
set(SDL2IMAGE_AVIF 1)
|
||||
set(SDL2IMAGE_BMP 1)
|
||||
set(SDL2IMAGE_GIF 1)
|
||||
set(SDL2IMAGE_JPG 1)
|
||||
set(SDL2IMAGE_JXL 1)
|
||||
set(SDL2IMAGE_LBM 1)
|
||||
set(SDL2IMAGE_PCX 1)
|
||||
set(SDL2IMAGE_PNG 1)
|
||||
set(SDL2IMAGE_PNM 1)
|
||||
set(SDL2IMAGE_QOI 1)
|
||||
set(SDL2IMAGE_SVG 1)
|
||||
set(SDL2IMAGE_TGA 1)
|
||||
set(SDL2IMAGE_TIF 1)
|
||||
set(SDL2IMAGE_XCF 1)
|
||||
set(SDL2IMAGE_XPM 1)
|
||||
set(SDL2IMAGE_XV 1)
|
||||
set(SDL2IMAGE_WEBP 1)
|
||||
|
||||
set(SDL2IMAGE_JPG_SAVE 1)
|
||||
set(SDL2IMAGE_PNG_SAVE 1)
|
||||
|
||||
set(SDL2IMAGE_VENDORED FALSE)
|
||||
|
||||
set(SDL2IMAGE_BACKEND_IMAGEIO 0)
|
||||
set(SDL2IMAGE_BACKEND_STB 0)
|
||||
set(SDL2IMAGE_BACKEND_WIC 0)
|
||||
|
||||
|
||||
|
||||
|
||||
add_library(SDL2_image::SDL2_image INTERFACE IMPORTED)
|
||||
target_link_options(SDL2_image::SDL2_image INTERFACE "-sUSE_SDL_IMAGE=2")
|
||||
target_compile_options(SDL2_image::SDL2_image INTERFACE "-sUSE_SDL_IMAGE=2")
|
||||
add_library(SDL2_image::SDL2_image-static INTERFACE IMPORTED)
|
||||
target_link_options(SDL2_image::SDL2_image-static INTERFACE "-sUSE_SDL_IMAGE=2")
|
||||
target_compile_options(SDL2_image::SDL2_image-static INTERFACE "-sUSE_SDL_IMAGE=2")
|
135
web/shell.html
Normal file
135
web/shell.html
Normal file
|
@ -0,0 +1,135 @@
|
|||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Looper</title>
|
||||
<link rel="icon" type="image/svg" href="/icon.svg">
|
||||
<style>
|
||||
.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
|
||||
textarea.emscripten { font-family: monospace; width: 80%; }
|
||||
div.emscripten { text-align: center; }
|
||||
div.emscripten_border { border: 1px solid black; }
|
||||
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
|
||||
canvas.emscripten { border: 0px none; background-color: black; }
|
||||
|
||||
.spinner {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
margin: 0px auto;
|
||||
-webkit-animation: rotation .8s linear infinite;
|
||||
-moz-animation: rotation .8s linear infinite;
|
||||
-o-animation: rotation .8s linear infinite;
|
||||
animation: rotation 0.8s linear infinite;
|
||||
border-left: 10px solid rgb(0,150,240);
|
||||
border-right: 10px solid rgb(0,150,240);
|
||||
border-bottom: 10px solid rgb(0,150,240);
|
||||
border-top: 10px solid rgb(100,0,200);
|
||||
border-radius: 100%;
|
||||
background-color: rgb(200,100,250);
|
||||
}
|
||||
@-webkit-keyframes rotation {
|
||||
from {-webkit-transform: rotate(0deg);}
|
||||
to {-webkit-transform: rotate(360deg);}
|
||||
}
|
||||
@-moz-keyframes rotation {
|
||||
from {-moz-transform: rotate(0deg);}
|
||||
to {-moz-transform: rotate(360deg);}
|
||||
}
|
||||
@-o-keyframes rotation {
|
||||
from {-o-transform: rotate(0deg);}
|
||||
to {-o-transform: rotate(360deg);}
|
||||
}
|
||||
@keyframes rotation {
|
||||
from {transform: rotate(0deg);}
|
||||
to {transform: rotate(360deg);}
|
||||
}
|
||||
.fullpage {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas class="fullpage emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1>
|
||||
<div class="center">
|
||||
<figure style="overflow:visible;" id="spinner"><div class="spinner"></div><center style="margin-top:0.5em"><strong>emscripten</strong></center></figure>
|
||||
<div class="emscripten" id="status">Downloading...</div>
|
||||
<div class="emscripten">
|
||||
<progress value="0" max="100" id="progress" hidden=1></progress>
|
||||
</div>
|
||||
</div>
|
||||
</canvas>
|
||||
<input type="file" value="" hidden id="file-picker">
|
||||
<script type="text/javascript" src="https://js.puter.com/v2/"></script>
|
||||
<script type='text/javascript' src="/shell.js"></script>
|
||||
<script>
|
||||
var Module = {
|
||||
print: (function() {
|
||||
var element = document.getElementById('output');
|
||||
if (element) element.value = ''; // clear browser cache
|
||||
return (...args) => {
|
||||
var text = args.join(' ');
|
||||
// These replacements are necessary if you render to raw HTML
|
||||
//text = text.replace(/&/g, "&");
|
||||
//text = text.replace(/</g, "<");
|
||||
//text = text.replace(/>/g, ">");
|
||||
//text = text.replace('\n', '<br>', 'g');
|
||||
console.log(text);
|
||||
};
|
||||
})(),
|
||||
canvas: (() => {
|
||||
var canvas = document.getElementById('canvas');
|
||||
|
||||
// As a default initial behavior, pop up an alert when webgl context is lost. To make your
|
||||
// application robust, you may want to override this behavior before shipping!
|
||||
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
|
||||
canvas.addEventListener("webglcontextlost", (e) => { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
|
||||
|
||||
return canvas;
|
||||
})(),
|
||||
setStatus: (text) => {
|
||||
if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
|
||||
if (text === Module.setStatus.last.text) return;
|
||||
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
|
||||
var now = Date.now();
|
||||
if (m && now - Module.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon
|
||||
Module.setStatus.last.time = now;
|
||||
Module.setStatus.last.text = text;
|
||||
if (m) {
|
||||
text = m[1];
|
||||
progressElement.value = parseInt(m[2])*100;
|
||||
progressElement.max = parseInt(m[4])*100;
|
||||
progressElement.hidden = false;
|
||||
spinnerElement.hidden = false;
|
||||
} else {
|
||||
progressElement.value = null;
|
||||
progressElement.max = null;
|
||||
progressElement.hidden = true;
|
||||
if (!text) spinnerElement.hidden = true;
|
||||
}
|
||||
statusElement.innerHTML = text;
|
||||
},
|
||||
totalDependencies: 0,
|
||||
monitorRunDependencies: (left) => {
|
||||
this.totalDependencies = Math.max(this.totalDependencies, left);
|
||||
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
|
||||
}
|
||||
};
|
||||
Module.setStatus('Downloading...');
|
||||
window.onerror = () => {
|
||||
Module.setStatus('Exception thrown, see JavaScript console');
|
||||
spinnerElement.style.display = 'none';
|
||||
Module.setStatus = (text) => {
|
||||
if (text) console.error('[post-exception status] ' + text);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
{{{ SCRIPT }}}
|
||||
</body>
|
||||
</html>
|
206
web/shell.js
Normal file
206
web/shell.js
Normal file
|
@ -0,0 +1,206 @@
|
|||
|
||||
var statusElement = document.getElementById('status');
|
||||
var progressElement = document.getElementById('progress');
|
||||
var spinnerElement = document.getElementById('spinner');
|
||||
class FilePicker {
|
||||
/**
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
el;
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
visible = false;
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
cancelled = false;
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
closed = false;
|
||||
/**
|
||||
* @type {Array<string>|null}
|
||||
*/
|
||||
value = null;
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
loading = false;
|
||||
/**
|
||||
* Sets the filter of the file picker.
|
||||
* @param {String} filter
|
||||
*/
|
||||
setFilter(filter) {
|
||||
this.el.setAttribute("accept", filter)
|
||||
}
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
puterEnabled = false;
|
||||
async openPuterFile(file) {
|
||||
this.loading = true;
|
||||
this.value = null;
|
||||
this.closed = false;
|
||||
this.visible = false;
|
||||
this.cancelled = false;
|
||||
let fileData = this.openWasmFile(file.name)
|
||||
let filePath = fileData.path;
|
||||
let handle = fileData.handle;
|
||||
await this.writeBlob(handle, await file.read())
|
||||
this.value = [filePath];
|
||||
this.cancelled = false;
|
||||
this.closed = true;
|
||||
this.visible = false;
|
||||
this.loading = false;
|
||||
}
|
||||
show() {
|
||||
this.visible = true;
|
||||
this.closed = false;
|
||||
this.cancelled = false;
|
||||
this.value = "";
|
||||
if (this.puterEnabled) {
|
||||
puter.ui.showOpenFilePicker({"accept": this.el.getAttribute("accept"), "multiple": false}).then(this.openPuterFile)
|
||||
} else {
|
||||
this.el.click();
|
||||
}
|
||||
}
|
||||
wasCancelled() {
|
||||
return !this.visible && this.cancelled
|
||||
}
|
||||
wasClosed() {
|
||||
return !this.visible && this.closed;
|
||||
}
|
||||
wasConfirmed() {
|
||||
return !this.visible && this.closed && !this.cancelled;
|
||||
}
|
||||
isLoading() {
|
||||
|
||||
}
|
||||
getFileList() {
|
||||
if (this.wasConfirmed()) {
|
||||
return this.value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
getFirstFile() {
|
||||
if (this.wasConfirmed()) {
|
||||
if (this.value.length > 0) {
|
||||
return this.value[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
clearSelection() {
|
||||
this.closed = false;
|
||||
this.cancelled = false;
|
||||
this.visible = false;
|
||||
this.el.value = null;
|
||||
this.value = null;
|
||||
}
|
||||
makeWasmDir() {
|
||||
let chars ="0123456789bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ_";
|
||||
let output = "/"
|
||||
for (let i = 0; i < 16; i++) {
|
||||
output += chars.charAt((Math.random() * chars.length) % chars.length)
|
||||
}
|
||||
FS.mkdir(output)
|
||||
output += "/";
|
||||
return output;
|
||||
}
|
||||
openWasmFile(name, dir = null) {
|
||||
if (dir === null) {
|
||||
dir = this.makeWasmDir();
|
||||
}
|
||||
let filePath = dir + "/" + name;
|
||||
let file = FS.open(filePath, "w+");
|
||||
return {
|
||||
"path": filePath,
|
||||
"handle": file
|
||||
};
|
||||
}
|
||||
async writeBlob(file, blob) {
|
||||
let data = null;
|
||||
let reader = blob.stream().getReader();
|
||||
while (data !== undefined) {
|
||||
let data = (await reader.read());
|
||||
if (data.done) {
|
||||
break;
|
||||
}
|
||||
FS.write(file, data.value, 0, data.value.length);
|
||||
}
|
||||
FS.close(file);
|
||||
}
|
||||
constructor() {
|
||||
if (puter.auth.isSignedIn()) {
|
||||
this.puterEnabled = true;
|
||||
}
|
||||
this.el = document.getElementById("file-picker")
|
||||
this.el.addEventListener("cancel", () => {
|
||||
this.value = null;
|
||||
this.cancelled = true;
|
||||
this.closed = true;
|
||||
this.visible = false;
|
||||
})
|
||||
this.el.addEventListener("change", async () => {
|
||||
if (this.el.files.length > 0) {
|
||||
this.loading = true;
|
||||
this.value = null;
|
||||
this.closed = false;
|
||||
this.visible = false;
|
||||
this.cancelled = false;
|
||||
let output = this.makeWasmDir();
|
||||
let newValue = []
|
||||
for (let i = 0; i < this.el.files.length; i++) {
|
||||
let element = this.el.files[i];
|
||||
let fileData = this.openWasmFile(element.name, output);
|
||||
let file = fileData.handle;
|
||||
let filePath = fileData.path;
|
||||
await this.writeBlob(file, element);
|
||||
newValue = [...newValue, filePath]
|
||||
}
|
||||
/**
|
||||
* @type {string[]|null}
|
||||
*/
|
||||
let oldValues = this.value;
|
||||
if (oldValues !== null) {
|
||||
setTimeout(() => {
|
||||
for (let i = 0; i < oldValues.length; i++) {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
let value = oldValues[i];
|
||||
let lastSlash = value.lastIndexOf("/")
|
||||
if (lastSlash === value.length - 1) {
|
||||
value = value.substring(0, lastSlash - 1);
|
||||
lastSlash = value.lastIndexOf("/")
|
||||
}
|
||||
let parent = value.substring(0, lastSlash - 1)
|
||||
FS.unlink(value);
|
||||
if (FS.readdir(parent).length === 2) {
|
||||
FS.rmdir(parent)
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
this.value = newValue;
|
||||
this.cancelled = false;
|
||||
this.closed = true;
|
||||
this.visible = false;
|
||||
this.loading = false;
|
||||
} else {
|
||||
this.value = null;
|
||||
this.cancelled = true;
|
||||
this.closed = true;
|
||||
this.visible = false;
|
||||
}
|
||||
})
|
||||
puter.ui.onLaunchedWithItems(function(items) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
this.openPuterFile(files[i]);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
window.filePicker = new FilePicker()
|
Loading…
Reference in a new issue