Add web support, and fix a bug.

This commit is contained in:
Zachary Hall 2024-04-24 09:59:51 -07:00
parent 39dde288fe
commit ee3962bda2
24 changed files with 1271 additions and 344 deletions

View file

@ -1,5 +1,10 @@
cmake_minimum_required(VERSION 3.28) cmake_minimum_required(VERSION 3.28)
project(looper VERSION 1.0.0 LANGUAGES C CXX) 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 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(SDL_MIXER_X_STATIC ON CACHE BOOL "") 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_XMPLAY OFF CACHE BOOL "" FORCE)
set(BUILD_AUDACIOUS OFF CACHE BOOL "" FORCE) set(BUILD_AUDACIOUS OFF CACHE BOOL "" FORCE)
set(BUILD_V123 OFF CACHE BOOL "" FORCE) set(BUILD_V123 OFF CACHE BOOL "" FORCE)
set(BUILD_STATIC OFF CACHE BOOL "") set(JSONCPP_WITH_TESTS OFF CACHE BOOL "" FORCE)
option(USE_VGMSTREAM "Enable using the VGMStream libraries (Unimplemented)" OFF) 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 "")
endif()
find_package(PkgConfig) find_package(PkgConfig)
pkg_check_modules(jsoncpp IMPORTED_TARGET jsoncpp)
#add_subdirectory(subprojects/jsoncpp) #add_subdirectory(subprojects/jsoncpp)
find_package(SDL2 REQUIRED)
find_package(sdbus-c++ REQUIRED)
include(GNUInstallDirs) include(GNUInstallDirs)
add_subdirectory(subprojects/SDL-Mixer-X) add_subdirectory(subprojects/SDL-Mixer-X)
add_subdirectory(subprojects/vgmstream) add_subdirectory(subprojects/vgmstream)
set(EXTRA_LIBS libvgmstream_shared) if (DEFINED EMSCRIPTEN)
set(EXTRA_LIBS )
else()
set(EXTRA_LIBS libvgmstream_shared)
endif()
if(SDL_MIXER_X_STATIC) if(SDL_MIXER_X_STATIC)
set(SDL_MIXER_X_TARGET SDL2_mixer_ext_Static) set(SDL_MIXER_X_TARGET SDL2_mixer_ext_Static)
else() else()
set(SDL_MIXER_X_TARGET SDL2_mixer_ext) set(SDL_MIXER_X_TARGET SDL2_mixer_ext)
set(EXTRA_LIBS ${EXTRA_LIBS} ${SDL_MIXER_X_TARGET}) set(EXTRA_LIBS ${EXTRA_LIBS} ${SDL_MIXER_X_TARGET})
endif() 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}) add_custom_target(looper_assets COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/assets/update_assets.py WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
find_package(Git) find_package(Git)
if (Git_FOUND) if (Git_FOUND)
@ -54,6 +115,7 @@ endif()
if(CMAKE_BUILD_TYPE STREQUAL "Debug") if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(DEBUG ON) set(DEBUG ON)
endif() endif()
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
include(log) 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) #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() pop_fnstack()
endmacro() endmacro()
set(INC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} subprojects/vgmstream/src subprojects/vgmstream/src/base) 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 "") set(UI_BACKENDS "")
list(POP_FRONT UI_BACKENDS) list(POP_FRONT UI_BACKENDS)
macro(prefix_all) macro(prefix_all)
@ -113,7 +174,22 @@ prefix_all(LIBRARY_SOURCES
) )
add_library(liblooper STATIC ${LIBRARY_SOURCES}) add_library(liblooper STATIC ${LIBRARY_SOURCES})
target_include_directories(liblooper PUBLIC ${INC}) 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) macro(add_ui_backend)
set(ARGS ${ARGV}) set(ARGS ${ARGV})
list(POP_FRONT ARGS target) list(POP_FRONT ARGS target)
@ -134,30 +210,59 @@ macro(add_ui_backend)
endif() endif()
endif() endif()
endmacro() endmacro()
option(DISABLE_GTK_UI "Disables the GTK+ UI" OFF) macro(ui_backend_subdir)
option(DISABLE_IMGUI_UI "Disables the Dear ImGui UI" OFF) 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()
endmacro()
set(ENABLED_UIS ) set(ENABLED_UIS )
if (NOT DISABLE_IMGUI_UI) ui_backend_subdir(NAME "IMGUI" READABLE_NAME "Dear ImGui" SUBDIR backends/ui/imgui)
add_subdirectory(backends/ui/imgui) if (NOT DEFINED EMSCRIPTEN)
list(APPEND ENABLED_UIS "imgui") ui_backend_subdir(NAME "GTK" READABLE_NAME "GTK4" SUBDIR backends/ui/gtk)
endif()
if (NOT DISABLE_GTK_UI)
add_subdirectory(backends/ui/gtk)
list(APPEND ENABLED_UIS "gtk")
endif() endif()
execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/gen_ui_backend_inc.py ${ENABLED_UIS}) 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) prefix_all(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/ backend_glue.cpp main.cpp daemon_backend.cpp proxy_backend.cpp)
add_executable(looper ${SOURCES}) if(DEFINED EMSCRIPTEN)
add_dependencies(looper looper_assets ${UI_BACKENDS}) 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) find_program(ASCLI_EXE NAMES "appstreamcli" NO_CACHE)
if(${ASCLI_EXE} STREQUAL "ASCLIEXE-NOTFOUND") if(${ASCLI_EXE} STREQUAL "ASCLIEXE-NOTFOUND")
message("Cannot verify Appstream Metadata.") message("Cannot verify Appstream Metadata.")
else() else()
add_test(NAME "verify appstream metadata" COMMAND ${ASCLI_EXE} validate --no-net --pedantic "assets/com.complecwaft.Looper.metainfo.xml") add_test(NAME "verify appstream metadata" COMMAND ${ASCLI_EXE} validate --no-net --pedantic "assets/com.complecwaft.Looper.metainfo.xml")
endif() endif()
target_link_libraries(looper PUBLIC liblooper PkgConfig::jsoncpp ${UI_BACKENDS}) target_link_libraries(${TARGET_NAME} PUBLIC liblooper ${UI_BACKENDS})
install(TARGETS looper ${EXTRA_LIBS}) install(TARGETS ${TARGET_NAME} ${EXTRA_LIBS})
install(FILES assets/icon.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps/) if (NOT DEFINED EMSCRIPTEN)
install(FILES assets/com.complecwaft.Looper.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) install(FILES assets/icon.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps/)
install(FILES assets/com.complecwaft.Looper.metainfo.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo) install(FILES assets/com.complecwaft.Looper.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
install(DIRECTORY assets/translations/ TYPE LOCALE PATTERN "*" EXCLUDE PATTERN "looper.pot") 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()

View file

@ -1,4 +1,3 @@
if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm") if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm")
set(GLES_NORMALLY_REQUIRED_FOR_ARCHITECTURE ON) set(GLES_NORMALLY_REQUIRED_FOR_ARCHITECTURE ON)
elseif(${CMAKE_SYSTEM_PROCESSOR} MATCHES "aarch64") 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}) foreach(INCDIR IN ITEMS ${BACKEND_IMGUI_INC_BASE})
set(BACKEND_IMGUI_INC ${BACKEND_IMGUI_INC} ${CMAKE_CURRENT_SOURCE_DIR}/${INCDIR}) set(BACKEND_IMGUI_INC ${BACKEND_IMGUI_INC} ${CMAKE_CURRENT_SOURCE_DIR}/${INCDIR})
endforeach() endforeach()
find_package(SDL2 REQUIRED)
find_package(SDL2_image REQUIRED)
if(${USE_GLES}) if(${USE_GLES})
set(GLComponents GLES2) set(GLComponents GLES2)
set(GLTarget GLES2) set(GLTarget GLES2)
@ -40,6 +37,13 @@ add_ui_backend(imgui_ui ${BACKEND_IMGUI_SRC})
if(${USE_GLES}) if(${USE_GLES})
target_compile_definitions(imgui_ui PRIVATE "IMGUI_IMPL_OPENGL_ES2") target_compile_definitions(imgui_ui PRIVATE "IMGUI_IMPL_OPENGL_ES2")
endif() endif()
target_link_libraries(imgui_ui PRIVATE OpenGL::${GLTarget} SDL2::SDL2 SDL2_image::SDL2_image) 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_include_directories(imgui_ui PRIVATE ${BACKEND_IMGUI_INC})
target_compile_definitions(imgui_ui PRIVATE IMGUI_USER_CONFIG="imgui_config.h") target_compile_definitions(imgui_ui PRIVATE IMGUI_USER_CONFIG="imgui_config.h")

View file

@ -14,7 +14,119 @@
#include "translation.h" #include "translation.h"
#include <log.hpp> #include <log.hpp>
using std::vector; 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 { struct FontData {
const char* data; const char* data;
const ImWchar *ranges; 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); 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(); ImGui_ImplOpenGL3_CreateFontsTexture();
} }
static EM_BOOL resize_callback(int event_type, const EmscriptenUiEvent *event, void *userdata) {
RendererBackend::resize_static();
}
int RendererBackend::Run() { int RendererBackend::Run() {
setlocale(LC_ALL, ""); setlocale(LC_ALL, "");
bindtextdomain("neko_player", LOCALE_DIR); bindtextdomain("neko_player", LOCALE_DIR);
@ -168,7 +283,7 @@ int RendererBackend::Run() {
const vector<unsigned char> icon_data = DecodeBase85(icon_compressed_data_base85); 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_Surface* icon = IMG_Load_RW(SDL_RWFromConstMem(icon_data.data(), icon_data.size()), 1);
SDL_SetWindowIcon(window, icon); SDL_SetWindowIcon(window, icon);
SDL_GLContext gl_context = SDL_GL_CreateContext(window); gl_context = SDL_GL_CreateContext(window);
SDL_GL_MakeCurrent(window, gl_context); SDL_GL_MakeCurrent(window, gl_context);
// Setup Dear ImGui context // Setup Dear ImGui context
@ -217,102 +332,26 @@ int RendererBackend::Run() {
"HOME" "HOME"
#endif #endif
); );
#ifndef __EMSCRIPTEN__
SDL_GL_SetSwapInterval(vsync ? 1 : 0); SDL_GL_SetSwapInterval(vsync ? 1 : 0);
#endif
theme->Apply(accent_color); theme->Apply(accent_color);
Init(); Init();
SDL_ShowWindow(window); SDL_ShowWindow(window);
renderer_backend = this;
#ifdef __EMSCRIPTEN__ #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. // 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. // You may manually call LoadIniSettingsFromMemory() to load settings from your own storage.
io.IniFilename = nullptr; 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 #else
while (!done) while (!done)
#endif
{ {
auto next_frame = std::chrono::steady_clock::now() + std::chrono::milliseconds(1000 / framerate); LoopFunction();
// 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 BackendDeinit();
#ifdef __EMSCRIPTEN__
EMSCRIPTEN_MAINLOOP_END;
#endif #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; return 0;
} }
void RendererBackend::Init() { void RendererBackend::Init() {

View file

@ -8,11 +8,22 @@
#endif #endif
#include <SDL.h> #include <SDL.h>
#include <SDL_video.h> #include <SDL_video.h>
#ifdef __EMSCRIPTEN__
#include "emscripten_mainloop_stub.h"
#include <emscripten.h>
#include <emscripten/html5.h>
#endif
#include <string> #include <string>
#include "theme.h" #include "theme.h"
static const char* NAME = "Looper"; static const char* NAME = "Looper";
class RendererBackend { class RendererBackend {
void BackendDeinit();
void LoopFunction();
SDL_GLContext gl_context;
bool resize_needed = true;
void on_resize();
public: public:
static void resize_static();
double scale = 1.0; double scale = 1.0;
SDL_Window *window; SDL_Window *window;
int window_width = 475; int window_width = 475;
@ -39,4 +50,5 @@ class RendererBackend {
void GetWindowsize(int *w, int *h); void GetWindowsize(int *w, int *h);
RendererBackend(); RendererBackend();
~RendererBackend(); ~RendererBackend();
friend void main_loop();
}; };

View 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

View file

@ -2,7 +2,21 @@
#include <sstream> #include <sstream>
#include <iomanip> #include <iomanip>
#include <cctype> #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) { FileBrowser::FileBrowser(bool save, ImGuiFileBrowserFlags extra_fallback_flags) {
#ifdef PORTALS #ifdef PORTALS
main_context = g_main_context_default(); 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)"); inner_filter_type = g_variant_type_new("a(us)");
#endif #endif
this->save = save; 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) { void FileBrowser::SetTypeFilters(string name, vector<string> filters) {
filter_name = name; filter_name = name;
this->filters = filters; 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 #ifdef PORTALS
if (variant != NULL) { if (variant != NULL) {
g_variant_unref(variant); g_variant_unref(variant);
@ -51,6 +75,8 @@ void FileBrowser::SetPwd(path path) {
bool FileBrowser::HasSelected() { bool FileBrowser::HasSelected() {
#ifdef PORTALS #ifdef PORTALS
return selected.has_value(); return selected.has_value();
#elif defined(__EMSCRIPTEN__)
return file_picker_confirmed();
#else #else
return fallback.HasSelected(); return fallback.HasSelected();
#endif #endif
@ -58,6 +84,15 @@ bool FileBrowser::HasSelected() {
path FileBrowser::GetSelected() { path FileBrowser::GetSelected() {
#ifdef PORTALS #ifdef PORTALS
return selected.value_or(path()); 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 #else
return fallback.GetSelected(); return fallback.GetSelected();
#endif #endif
@ -78,6 +113,8 @@ void FileBrowser::Open() {
} else { } else {
xdp_portal_open_file(portal, NULL, title.c_str(), variant, NULL, NULL, XDP_OPEN_FILE_FLAG_NONE, NULL, &FileBrowser::FileBrowserOpenCallback, this); 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 #else
fallback.Open(); fallback.Open();
#endif #endif
@ -157,6 +194,47 @@ void FileBrowser::FileBrowserSaveCallback(GObject *src, GAsyncResult *res, gpoin
void FileBrowser::Display() { void FileBrowser::Display() {
#ifdef PORTALS #ifdef PORTALS
g_main_context_iteration(main_context, false); 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 #else
fallback.Display(); fallback.Display();
#endif #endif
@ -164,6 +242,9 @@ void FileBrowser::Display() {
void FileBrowser::ClearSelected() { void FileBrowser::ClearSelected() {
selected = optional<path>(); selected = optional<path>();
#ifdef __EMSCRIPTEN__
clear_file_selection();
#endif
#ifndef PORTALS #ifndef PORTALS
fallback.ClearSelected(); fallback.ClearSelected();
#endif #endif
@ -177,6 +258,8 @@ void FileBrowser::SetTitle(string title) {
bool FileBrowser::IsOpened() { bool FileBrowser::IsOpened() {
#ifdef PORTALS #ifdef PORTALS
return open; return open;
#elif defined(__EMSCRIPTEN__)
return !file_picker_closed() || file_picker_confirmed();
#else #else
return fallback.IsOpened(); return fallback.IsOpened();
#endif #endif

View file

@ -4,6 +4,9 @@
#include <vector> #include <vector>
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#ifdef PORTALS #ifdef PORTALS
#include <libportal/portal.h> #include <libportal/portal.h>
#include <libportal/filechooser.h> #include <libportal/filechooser.h>
@ -27,6 +30,7 @@ class FileBrowser {
static void FileBrowserOpenCallback(GObject *src, GAsyncResult *res, gpointer data); static void FileBrowserOpenCallback(GObject *src, GAsyncResult *res, gpointer data);
static void FileBrowserSaveCallback(GObject *src, GAsyncResult *res, gpointer data); static void FileBrowserSaveCallback(GObject *src, GAsyncResult *res, gpointer data);
#endif #endif
ImGuiFileBrowserFlags flags;
optional<ImVec2> window_pos; optional<ImVec2> window_pos;
ImVec2 window_size; ImVec2 window_size;
bool open = false; bool open = false;

View file

@ -6,7 +6,12 @@
#include <options.hpp> #include <options.hpp>
#include "ui_backend.hpp" #include "ui_backend.hpp"
#include "thirdparty/CLI11.hpp" #include "thirdparty/CLI11.hpp"
#ifdef __EMSCRIPTEN__
extern "C" {
extern bool is_puter_enabled();
extern void enable_puter(bool enable);
}
#endif
void MainLoop::Init() { void MainLoop::Init() {
#ifdef PORTALS #ifdef PORTALS
@ -16,7 +21,9 @@ void MainLoop::Init() {
show_demo_window = false; show_demo_window = false;
FileBrowser fileDialog(false, ImGuiFileBrowserFlags_NoTitleBar|ImGuiFileBrowserFlags_NoMove|ImGuiFileBrowserFlags_NoResize); FileBrowser fileDialog(false, ImGuiFileBrowserFlags_NoTitleBar|ImGuiFileBrowserFlags_NoMove|ImGuiFileBrowserFlags_NoResize);
#ifndef __EMSCRIPTEN__
fileDialog.SetPwd(path(userdir) / path("Music")); fileDialog.SetPwd(path(userdir) / path("Music"));
#endif
fileDialog.SetWindowSize(window_width, window_height); fileDialog.SetWindowSize(window_width, window_height);
//fileDialog.SetWindowPos(0, 0); //fileDialog.SetWindowPos(0, 0);
position = 0.0; position = 0.0;
@ -91,23 +98,28 @@ void MainLoop::Init() {
} }
} }
theme->Apply(accent_color); theme->Apply(accent_color);
FileLoaded();
} }
void MainLoop::Drop(std::string file) { void MainLoop::Drop(std::string file) {
LoadFile(file); LoadFile(file);
} }
void MainLoop::FileLoaded() {
auto file_maybe = playback->get_current_title();
if (file_maybe.has_value()) {
auto name = file_maybe.value();
SetWindowTitle((name + std::string(" - Looper")).c_str());
} else {
SetWindowTitle("Looper");
}
streams = playback->get_streams();
}
void MainLoop::GuiFunction() { void MainLoop::GuiFunction() {
playback->LoopHook();
position = playback->GetPosition(); position = playback->GetPosition();
length = playback->GetLength(); length = playback->GetLength();
// Set the window title if the file changed, or playback stopped. // Set the window title if the file changed, or playback stopped.
if (playback->handle_signals(PlaybackSignalFileChanged|PlaybackSignalStopped)) { if (playback->handle_signals(PlaybackSignalFileChanged|PlaybackSignalStarted|PlaybackSignalStopped)) {
auto file_maybe = playback->get_current_title(); FileLoaded();
if (file_maybe.has_value()) {
auto name = file_maybe.value();
SetWindowTitle((name + std::string(" - Looper")).c_str());
} else {
SetWindowTitle("Looper");
}
streams = playback->get_streams();
} }
bool lengthKnown = length > 0.0; bool lengthKnown = length > 0.0;
auto dockid = ImGui::DockSpaceOverViewport(nullptr, ImGuiDockNodeFlags_PassthruCentralNode|ImGuiDockNodeFlags_AutoHideTabBar); auto dockid = ImGui::DockSpaceOverViewport(nullptr, ImGuiDockNodeFlags_PassthruCentralNode|ImGuiDockNodeFlags_AutoHideTabBar);
@ -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))) { 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; 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; static bool override_lang = lang != DEFAULT_LANG;
if (ImGui::Checkbox(_TR_CTX("Preference | override enable checkbox", "Override language"), &override_lang)) { if (ImGui::Checkbox(_TR_CTX("Preference | override enable checkbox", "Override language"), &override_lang)) {
if (!override_lang) { if (!override_lang) {

View file

@ -30,7 +30,7 @@
#include "imgui/misc/cpp/imgui_stdlib.h" #include "imgui/misc/cpp/imgui_stdlib.h"
#include "translation.h" #include "translation.h"
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
#include "../libs/emscripten/emscripten_mainloop_stub.h" #include "emscripten_mainloop_stub.h"
#endif #endif
#include "../../../backend.hpp" #include "../../../backend.hpp"
#include "ui_backend.hpp" #include "ui_backend.hpp"
@ -57,6 +57,7 @@ class MainLoop : public RendererBackend {
public: public:
Playback *playback; Playback *playback;
vector<std::string> args; vector<std::string> args;
void FileLoaded();
void LoadFile(std::string file); void LoadFile(std::string file);
void Init() override; void Init() override;
void GuiFunction() override; void GuiFunction() override;

View file

@ -1,3 +1,4 @@
#ifdef DBUS_ENABLED
#include "daemon_backend.hpp" #include "daemon_backend.hpp"
#include "log.hpp" #include "log.hpp"
#include <thread> #include <thread>
@ -27,3 +28,4 @@ int DaemonGlueBackend::run(std::vector<std::string> realArgs, int argc, char **a
} }
return 0; return 0;
} }
#endif

View file

@ -1,4 +1,5 @@
#pragma once #pragma once
#ifdef DBUS_ENABLED
#include "backend.hpp" #include "backend.hpp"
class DaemonGlueBackend : public UIBackend { class DaemonGlueBackend : public UIBackend {
public: public:
@ -7,3 +8,4 @@ class DaemonGlueBackend : public UIBackend {
std::string get_name() override; std::string get_name() override;
int run(std::vector<std::string> realArgs, int argc, char **argv) override; int run(std::vector<std::string> realArgs, int argc, char **argv) override;
}; };
#endif

View file

@ -2,6 +2,7 @@
#include "log.hpp" #include "log.hpp"
#include "backend.hpp" #include "backend.hpp"
#include <random> #include <random>
#ifdef DBUS_ENABLED
MprisAPI::MprisAPI(sdbus::IConnection &connection, std::string objectPath, DBusAPI *dbus_api) MprisAPI::MprisAPI(sdbus::IConnection &connection, std::string objectPath, DBusAPI *dbus_api)
: AdaptorInterfaces(connection, std::move(objectPath)) : AdaptorInterfaces(connection, std::move(objectPath))
, dbus_api(dbus_api) , dbus_api(dbus_api)
@ -31,7 +32,9 @@ void MprisAPI::Seek(const int64_t &offset) {
dbus_api->Position(value); dbus_api->Position(value);
} }
void MprisAPI::SetPosition(const sdbus::ObjectPath &TrackId, const int64_t &offset) { void MprisAPI::SetPosition(const sdbus::ObjectPath &TrackId, const int64_t &offset) {
Seek(offset); if (TrackId == playing_track_id) {
Seek(offset);
}
} }
void MprisAPI::OpenUri(const std::string &Uri) { void MprisAPI::OpenUri(const std::string &Uri) {
dbus_api->Start(Uri, true); dbus_api->Start(Uri, true);
@ -129,6 +132,13 @@ bool MprisAPI::CanEditTracks() {
MprisAPI::~MprisAPI() { MprisAPI::~MprisAPI() {
unregisterAdaptor(); 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) DBusAPI::DBusAPI(Playback *playback, sdbus::IConnection &connection, std::string objectPath, bool daemon)
: AdaptorInterfaces(connection, std::move(objectPath)) : AdaptorInterfaces(connection, std::move(objectPath))
, daemon(daemon) , daemon(daemon)
@ -148,13 +158,19 @@ DBusAPI::DBusAPI(Playback *playback, sdbus::IConnection &connection, std::string
}); });
connection.enterEventLoopAsync(); connection.enterEventLoopAsync();
} }
#endif
const char *DBusAPI::objectPath = "/com/complecwaft/Looper"; const char *DBusAPI::objectPath = "/com/complecwaft/Looper";
const char *DBusAPI::busName = "com.complecwaft.Looper"; const char *DBusAPI::busName = "com.complecwaft.Looper";
DBusAPI *DBusAPI::Create(Playback *playback, bool daemon) { DBusAPI *DBusAPI::Create(Playback *playback, bool daemon) {
#ifdef DBUS_ENABLED
auto connection = sdbus::createSessionBusConnection(busName); auto connection = sdbus::createSessionBusConnection(busName);
auto &con_ref = *connection.release(); auto &con_ref = *connection.release();
return new DBusAPI(playback, con_ref, objectPath, daemon); return new DBusAPI(playback, con_ref, objectPath, daemon);
#else
return new DBusAPI(playback, daemon);
#endif
} }
#ifdef DBUS_ENABLED
double DBusAPI::Position() { double DBusAPI::Position() {
return playback->GetPosition(); 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) { void DBusAPI::PlayStream(const uint32_t &idx) {
playback->play_stream((int)idx); playback->play_stream((int)idx);
} }
#endif
DBusAPI::~DBusAPI() { DBusAPI::~DBusAPI() {
#ifdef DBUS_ENABLED
threadExitFlag.store(true); threadExitFlag.store(true);
threadFunc.join(); threadFunc.join();
unregisterAdaptor(); unregisterAdaptor();
#endif
} }
bool DBusAPISender::isOnlyInstance() { bool DBusAPISender::isOnlyInstance() {
#ifdef DBUS_ENABLED
bool output; bool output;
try { try {
auto *tmp = Create(); auto *tmp = Create();
@ -432,7 +452,11 @@ bool DBusAPISender::isOnlyInstance() {
} catch (sdbus::Error) { } catch (sdbus::Error) {
return true; return true;
} }
#else
return true;
#endif
} }
#ifdef DBUS_ENABLED
std::optional<std::string> DBusAPISender::get_current_file() { std::optional<std::string> DBusAPISender::get_current_file() {
if (IsStopped()) { if (IsStopped()) {
@ -544,7 +568,9 @@ float DBusAPISender::GetVolume() {
void DBusAPISender::Update() { void DBusAPISender::Update() {
} }
#endif
DBusAPISender *DBusAPISender::Create() { DBusAPISender *DBusAPISender::Create() {
#ifdef DBUS_ENABLED
try { try {
auto connection = sdbus::createSessionBusConnection(); auto connection = sdbus::createSessionBusConnection();
auto &con_ref = *connection.release(); 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()); DEBUG.writefln("sdbus::Error: %s: %s", error.getName().c_str(), error.getMessage().c_str());
return nullptr; return nullptr;
} }
#else
return nullptr;
#endif
} }
#ifdef DBUS_ENABLED
DBusAPISender::DBusAPISender(sdbus::IConnection &connection, std::string busName, std::string objectPath) DBusAPISender::DBusAPISender(sdbus::IConnection &connection, std::string busName, std::string objectPath)
: ProxyInterfaces(connection, std::move(busName), std::move(objectPath)) { : ProxyInterfaces(connection, std::move(busName), std::move(objectPath)) {
registerProxy(); registerProxy();
@ -584,3 +614,4 @@ DBusAPISender::~DBusAPISender() {
ClearHandle(handle); ClearHandle(handle);
unregisterProxy(); unregisterProxy();
} }
#endif

View file

@ -1,14 +1,17 @@
#pragma once #pragma once
#ifdef DBUS_ENABLED
#include <sdbus-c++/sdbus-c++.h> #include <sdbus-c++/sdbus-c++.h>
#include <sdbus-c++/StandardInterfaces.h> #include <sdbus-c++/StandardInterfaces.h>
#include "assets/dbus_stub_adaptor.hpp" #include "assets/dbus_stub_adaptor.hpp"
#include "assets/dbus_stub_proxy.hpp" #include "assets/dbus_stub_proxy.hpp"
#include "assets/mpris_stub_adaptor.hpp" #include "assets/mpris_stub_adaptor.hpp"
#endif
#include "playback.h" #include "playback.h"
#include <mutex> #include <mutex>
#include <optional> #include <optional>
#include <random> #include <random>
#include <thread> #include <thread>
#ifdef DBUS_ENABLED
class DBusAPI; class DBusAPI;
class MprisAPI : public sdbus::AdaptorInterfaces<org::mpris::MediaPlayer2_adaptor, org::mpris::MediaPlayer2::Player_adaptor, org::mpris::MediaPlayer2::TrackList_adaptor, sdbus::Properties_adaptor> { 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; 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(sdbus::IConnection &connection, std::string objectPath, DBusAPI *dbus_api);
~MprisAPI(); ~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; std::map<std::string, void*> handles;
size_t handle_idx = 0; size_t handle_idx = 0;
public:
static const char *objectPath;
static const char *busName;
#ifdef DBUS_ENABLED
private:
MprisAPI *mpris; MprisAPI *mpris;
sdbus::IConnection &connection;
std::minstd_rand rand_engine; std::minstd_rand rand_engine;
std::deque<std::string> *get_errors_by_handle(const std::string &handle); std::deque<std::string> *get_errors_by_handle(const std::string &handle);
bool daemon; bool daemon;
std::atomic_bool threadExitFlag = false; std::atomic_bool threadExitFlag = false;
std::thread threadFunc; std::thread threadFunc;
sdbus::IConnection &connection;
public: public:
static const char *objectPath;
static const char *busName;
void Activate(const std::map<std::string, sdbus::Variant>& platform_data) override; 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 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; 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; std::vector<sdbus::Struct<double, std::string, int32_t>> GetStreams() override;
void PlayStream(const uint32_t &idx) override; void PlayStream(const uint32_t &idx) override;
#endif
public:
// API // API
Playback *playback; Playback *playback;
#ifdef DBUS_ENABLED
void Update(); void Update();
DBusAPI(Playback *playback, sdbus::IConnection &connection, std::string objectPath, bool daemon); DBusAPI(Playback *playback, sdbus::IConnection &connection, std::string objectPath, bool daemon);
#endif
DBusAPI(Playback *playback, bool daemon);
~DBusAPI(); ~DBusAPI();
static DBusAPI *Create(Playback *playback, bool daemon = false); 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 // Cache
double length, pitch, speed, tempo, volume; double length, pitch, speed, tempo, volume;
bool stopped, paused; 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. /// @returns A proxy to the main instance of the playback engine, or nullptr if there is none.
static DBusAPISender *Create(); static DBusAPISender *Create();
#ifdef DBUS_ENABLED
// Signals. Protected so that they aren't seen as a proper API // Signals. Protected so that they aren't seen as a proper API
protected: protected:
void onPlaybackEngineStarted() override; 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); DBusAPISender(sdbus::IConnection &connection, std::string busName, std::string objectPath);
public: public:
~DBusAPISender(); ~DBusAPISender();
#else
public:
~DBusAPISender() = default;
#endif
}; };

View file

@ -10,6 +10,11 @@
using namespace Looper; using namespace Looper;
using namespace Looper::Options; using namespace Looper::Options;
using namespace Looper::Log; using namespace Looper::Log;
#ifdef __EMSCRIPTEN__
extern "C" {
void quit();
}
#endif
std::unordered_set<LicenseData> license_data; std::unordered_set<LicenseData> license_data;
std::unordered_set<LicenseData> &get_license_data() { std::unordered_set<LicenseData> &get_license_data() {
return license_data; return license_data;
@ -25,15 +30,17 @@ int main(int argc, char **argv) {
int log_level; int log_level;
std::string ui_backend_option = ""; std::string ui_backend_option = "";
bool full_help = false; bool full_help = false;
bool daemonize = false;
bool disable_gui = false;
bool quit = false;
bool open_window = 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("-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."); 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("-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("-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."); app.add_flag("-q, --quit", quit, "Quits an existing instance.");
#endif
try { try {
app.parse(args); app.parse(args);
} catch (const CLI::ParseError &e) { } catch (const CLI::ParseError &e) {
@ -43,15 +50,18 @@ int main(int argc, char **argv) {
exit(app.exit(e)); exit(app.exit(e));
} }
} }
#ifdef DBUS_ENABLED
if (daemonize) { if (daemonize) {
ui_backend_option = "daemon"; ui_backend_option = "daemon";
} }
#endif
args.clear(); args.clear();
args = app.remaining(false); args = app.remaining(false);
int new_argc = args.size(); int new_argc = args.size();
char **new_argv = (char**)malloc(new_argc * sizeof(char*)); char **new_argv = (char**)malloc(new_argc * sizeof(char*));
init_logging(); init_logging();
#ifdef DBUS_ENABLED
if (quit) { if (quit) {
DBusAPISender *sender = DBusAPISender::Create(); DBusAPISender *sender = DBusAPISender::Create();
if (sender != nullptr) { if (sender != nullptr) {
@ -62,6 +72,7 @@ int main(int argc, char **argv) {
} }
return 0; return 0;
} }
#endif
{ {
auto looper_mit = LicenseData("Looper (MIT)", "MIT"); auto looper_mit = LicenseData("Looper (MIT)", "MIT");
auto looper_gpl = LicenseData("Looper (GPL)", "GPL-3.0-or-later"); 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..."); DEBUG.writeln("Initializing frontends...");
init_backends(); init_backends();
#ifdef DBUS_ENABLED
ProxyGlueBackend *proxy_backend = nullptr; ProxyGlueBackend *proxy_backend = nullptr;
if ((disable_gui && !daemonize) || quit) { if ((disable_gui && !daemonize) || quit) {
if (!DBusAPISender::isOnlyInstance()) { if (!DBusAPISender::isOnlyInstance()) {
@ -111,6 +123,7 @@ int main(int argc, char **argv) {
if (daemonize) { if (daemonize) {
UIBackend::register_backend<DaemonGlueBackend>(); UIBackend::register_backend<DaemonGlueBackend>();
} }
#endif
for (auto kv : UIBackend::backends) { for (auto kv : UIBackend::backends) {
kv.second->add_licenses(); kv.second->add_licenses();
} }
@ -134,6 +147,7 @@ int main(int argc, char **argv) {
args.push_back("--help"); args.push_back("--help");
} }
try { try {
#ifdef DBUS_ENABLED
if (proxy_backend != nullptr && !quit) { if (proxy_backend != nullptr && !quit) {
if (!proxy_backend->run(args, new_argc, new_argv)) { if (!proxy_backend->run(args, new_argc, new_argv)) {
throw 0; throw 0;
@ -143,11 +157,14 @@ int main(int argc, char **argv) {
if (!quit) { if (!quit) {
UIBackend::unregister_backend<ProxyGlueBackend>(); UIBackend::unregister_backend<ProxyGlueBackend>();
} }
#endif
output = backend->run(args, new_argc, new_argv); output = backend->run(args, new_argc, new_argv);
#ifdef DBUS_ENABLED
if (quit && proxy_backend != nullptr) { if (quit && proxy_backend != nullptr) {
proxy_backend->quitDaemon(); proxy_backend->quitDaemon();
proxy_backend->unregister_self(); proxy_backend->unregister_self();
} }
#endif
} catch (int return_code) { } catch (int return_code) {
if (full_help) { if (full_help) {
std::string helpstr = app.help(); std::string helpstr = app.help();
@ -168,5 +185,9 @@ int main(int argc, char **argv) {
} }
free(new_argv); free(new_argv);
save_options(); save_options();
#ifdef __EMSCRIPTEN__
quit();
#endif
return output; return output;
} }

View file

@ -17,6 +17,8 @@ extern "C" {
#include "log.hpp" #include "log.hpp"
#include <filesystem> #include <filesystem>
#include "dbus.hpp" #include "dbus.hpp"
#include <format>
#include "util.hpp"
using namespace std::chrono; using namespace std::chrono;
int NotSDL_ConvertAudioSamples(const SDL_AudioSpec *src_spec, const Uint8 *src_data, int src_len, 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); close_vgmstream(output);
stream_list_mutex.lock(); stream_list_mutex.lock();
streams.clear(); streams.clear();
PlaybackStream defaultStream; //PlaybackStream defaultStream;
defaultStream.id = 0; //defaultStream.id = 0;
defaultStream.name = "Default"; //defaultStream.name = "Default";
streams.push_back(defaultStream); //streams.push_back(defaultStream);
for (int i = 0; i <= stream_count; i++) { for (int i = 0; i <= stream_count; i++) {
PlaybackStream stream; PlaybackStream stream;
stream.id = i; stream.id = i;
@ -208,12 +210,17 @@ VGMSTREAM *PlaybackInstance::LoadVgm(const char *file, int idx) {
char *buf = (char*)malloc(STREAM_NAME_SIZE + 1); char *buf = (char*)malloc(STREAM_NAME_SIZE + 1);
memset(buf, 0, STREAM_NAME_SIZE + 1); memset(buf, 0, STREAM_NAME_SIZE + 1);
strncpy(buf, tmp->stream_name, STREAM_NAME_SIZE); strncpy(buf, tmp->stream_name, STREAM_NAME_SIZE);
if (buf[0] == '\0') {
free(buf);
buf = strdup("Unknown");
}
if (i == 0) { if (i == 0) {
stream.name = std::format("Default ({})", buf); stream.name = std::format("Default ({})", buf);
} else { } else {
stream.name = buf; 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); free(buf);
close_vgmstream(tmp); close_vgmstream(tmp);
} }
@ -298,216 +305,229 @@ void PlaybackInstance::UpdateST() {
double PlaybackInstance::GetMaxSeconds() { double PlaybackInstance::GetMaxSeconds() {
return std::max((double)(MaxSpeed * MaxTempo), st->getInputOutputSampleRatio()); return std::max((double)(MaxSpeed * MaxTempo), st->getInputOutputSampleRatio());
} }
void PlaybackInstance::ThreadFunc() { void PlaybackInstance::InitLoopFunction() {
#ifdef __linux__
pthread_setname_np(pthread_self(), "Playback control thread");
#endif
bool reload = false; bool reload = false;
speed_changed.store(true); speed_changed.store(true);
tempo_changed.store(true); tempo_changed.store(true);
pitch_changed.store(true); pitch_changed.store(true);
while (running) { playback_ready.store(false);
playback_ready.store(false); if (!SDL_WasInit(SDL_INIT_AUDIO)) {
if (!SDL_WasInit(SDL_INIT_AUDIO)) { if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { ERROR.writefln("Error initializing SDL: '%s'", SDL_GetError());
ERROR.writefln("Error initializing SDL: '%s'", SDL_GetError()); set_error("Failed to initialize SDL!");
set_error("Failed to initialize SDL!"); return;
return;
}
} }
SDL_AudioSpec obtained; }
SDL_AudioSpec desired; SDL_AudioSpec obtained;
desired.format = SDL_AudioSpec desired;
#ifdef SOUNDTOUCH_INTEGER_SAMPLES desired.format =
AUDIO_S16SYS; #ifdef SOUNDTOUCH_INTEGER_SAMPLES
#else AUDIO_S16SYS;
AUDIO_F32SYS; #else
#endif AUDIO_F32SYS;
desired.freq = 48000; #endif
desired.samples = 1024; desired.freq = 48000;
desired.channels = 2; desired.samples = 1024;
desired.callback = PlaybackInstance::SDLCallback; desired.channels = 2;
desired.userdata = this; desired.callback = PlaybackInstance::SDLCallback;
st = new SoundTouch(); desired.userdata = this;
Mix_Init(MIX_INIT_FLAC|MIX_INIT_MID|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG|MIX_INIT_OPUS|MIX_INIT_WAVPACK); st = new SoundTouch();
if ((device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, SDL_AUDIO_ALLOW_CHANNELS_CHANGE|SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_SAMPLES_CHANGE)) == 0) { Mix_Init(MIX_INIT_FLAC|MIX_INIT_MID|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG|MIX_INIT_OPUS|MIX_INIT_WAVPACK);
ERROR.writefln("Error opening audio device: '%s'", SDL_GetError()); if ((device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, SDL_AUDIO_ALLOW_CHANNELS_CHANGE|SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_SAMPLES_CHANGE)) == 0) {
set_error("Failed to open audio device!"); ERROR.writefln("Error opening audio device: '%s'", SDL_GetError());
running = false; set_error("Failed to open audio device!");
break; running = false;
} loop_started = false;
spec = obtained; return;
st->setSampleRate(spec.freq); }
st->setChannels(spec.channels); spec = obtained;
UpdateST(); st->setSampleRate(spec.freq);
bufsize = 0; st->setChannels(spec.channels);
fakespec = spec; UpdateST();
double maxSeconds = GetMaxSeconds(); bufsize = 0;
fakespec.size *= maxSeconds; fakespec = spec;
fakespec.samples *= maxSeconds; double maxSeconds = GetMaxSeconds();
vgmstream_spec = fakespec; fakespec.size *= maxSeconds;
vgmstream_spec.format = AUDIO_S16; fakespec.samples *= maxSeconds;
size_t new_bufsize = CalculateBufSize(&spec, GetMaxSeconds(), MaxSeconds); vgmstream_spec = fakespec;
buf = (Uint8*)malloc(new_bufsize); vgmstream_spec.format = AUDIO_S16;
if (buf == nullptr) { size_t new_bufsize = CalculateBufSize(&spec, GetMaxSeconds(), MaxSeconds);
ERROR.writeln("Failed to allocate memory for playback!"); buf = (Uint8*)malloc(new_bufsize);
set_error("Failed to allocate memory for playback!"); if (buf == nullptr) {
set_signal(PlaybackSignalErrorOccurred); ERROR.writeln("Failed to allocate memory for playback!");
running = false; set_error("Failed to allocate memory for playback!");
break; set_signal(PlaybackSignalErrorOccurred);
} running = false;
bufsize = new_bufsize; loop_started = false;
general_mixer = Mix_GetGeneralMixer(); return;
Mix_InitMixer(&fakespec, SDL_FALSE); }
SDL_PauseAudioDevice(device, 0); bufsize = new_bufsize;
general_mixer = Mix_GetGeneralMixer();
Mix_InitMixer(&fakespec, SDL_FALSE);
SDL_PauseAudioDevice(device, 0);
music = LoadMix(filePath.c_str());
stream = nullptr;
if (music == nullptr) {
stream = LoadVgm(filePath.c_str(), 0); stream = LoadVgm(filePath.c_str(), 0);
Mix_Music *music = nullptr; }
if (stream == nullptr) {
music = LoadMix(filePath.c_str());
}
reload = false; reload = false;
if (music || stream) {
playback_ready.store(true);
} else {
playback_ready.store(false);
}
set_signal(PlaybackSignalStarted);
}
void PlaybackInstance::LoopFunction() {
if (file_changed.exchange(false) || load_requested.exchange(false)) {
if (stream != nullptr) {
UnloadVgm(stream);
}
if (music != nullptr) {
UnloadMix(music);
}
music = LoadMix(filePath.c_str());
stream = nullptr;
if (music == nullptr) {
stream = LoadVgm(filePath.c_str(), 0);
}
if (music || stream) { if (music || stream) {
playback_ready.store(true); playback_ready.store(true);
} else { } else {
playback_ready.store(false); playback_ready.store(false);
} }
set_signal(PlaybackSignalStarted);
while (running) {
if (file_changed.exchange(false) || load_requested.exchange(false)) {
if (stream != nullptr) {
UnloadVgm(stream);
}
if (music != nullptr) {
UnloadMix(music);
}
stream = LoadVgm(filePath.c_str(), 0);
if (stream == nullptr) {
music = LoadMix(filePath.c_str());
}
if (music || stream) {
playback_ready.store(true);
} else {
playback_ready.store(false);
}
}
if (stream_changed.exchange(false)) {
std::string file = current_file.value();
if (streams[current_stream].name == "" || streams[current_stream].length <= 0 || current_stream < 0 || current_stream >= streams.size()) {
if (stream != nullptr) {
current_stream = stream->stream_index;
} else {
current_stream = 0;
}
} else {
if (stream != nullptr) {
UnloadVgm(stream);
stream = LoadVgm(file.c_str(), current_stream);
} else if (music != nullptr) {
UnloadMix(music);
music = LoadMix(file.c_str());
}
}
if (music || stream) {
playback_ready.store(true);
} else {
playback_ready.store(false);
}
}
if (flag_mutex.try_lock()) {
if (seeking.exchange(false)) {
if (stream != nullptr) {
SDL_LockAudioDevice(device);
seek_vgmstream(stream, (int32_t)((double)stream->sample_rate * position));
st->flush();
SDL_UnlockAudioDevice(device);
} else {
Mix_SetMusicPositionStream(music, position);
}
set_signal(PlaybackSignalSeeked);
}
if (pause_changed.exchange(false)) {
if (stream != nullptr) {
SDL_PauseAudioDevice(device, paused ? 1 : 0);
}
if (paused) {
if (music != nullptr) {
Mix_PauseMusicStream(music);
}
set_signal(PlaybackSignalPaused);
} else {
if (music != nullptr) {
Mix_ResumeMusicStream(music);
}
set_signal(PlaybackSignalResumed);
}
}
if (update.exchange(false)) {
SDL_LockAudioDevice(device);
real_volume = volume / 100.0;
if (stream == nullptr) {
Mix_VolumeMusicStream(music, (int)(volume / 100.0 * MIX_MAX_VOLUME));
}
UpdateST();
size_t correct_buf_size = CalculateBufSize(&spec, GetMaxSeconds(), MaxSeconds);
size_t max_buf_size = correct_buf_size * 10;
bool too_large = max_buf_size < bufsize;
bool too_small = correct_buf_size > bufsize;
if (too_large) {
ERROR.writes("Bufsize is too large - ");
} else if (too_small) {
ERROR.writes("Bufsize is too small - ");
}
if (too_large || too_small) {
ERROR.writeln("Resizing buffer...");
general_mixer = nullptr;
bufsize = 0;
buf = (Uint8*)realloc((void*)buf, correct_buf_size);
if (buf == nullptr) {
ERROR.writes("Failed to allocate memory for playback!");
set_error("Failed to allocate memory for playback!");
set_signal(PlaybackSignalErrorOccurred);
running = false;
break;
}
bufsize = correct_buf_size;
}
SDL_UnlockAudioDevice(device);
}
flag_mutex.unlock();
}
if (stream != nullptr) {
double maybe_new_position = (double)stream->current_sample / stream->sample_rate;
if (position > maybe_new_position) {
position = maybe_new_position;
}
position += 0.02 * (speed * tempo);
} else if (music != nullptr) {
position = Mix_GetMusicPosition(music);
}
std::this_thread::sleep_for(20ms);
}
playback_ready.store(false);
// ====
if (music != nullptr) {
UnloadMix(music);
}
if (stream != nullptr) {
UnloadVgm(stream);
}
SDL_CloseAudioDevice(device);
Mix_CloseAudio();
Mix_Quit();
SDL_QuitSubSystem(SDL_INIT_AUDIO);
delete st;
free(buf);
} }
if (stream_changed.exchange(false)) {
std::string file = current_file.value();
if (streams[current_stream].name == "" || streams[current_stream].length <= 0 || current_stream < 0 || current_stream >= streams.size()) {
if (stream != nullptr) {
current_stream = stream->stream_index;
} else {
current_stream = 0;
}
} else {
if (stream != nullptr) {
UnloadVgm(stream);
stream = LoadVgm(file.c_str(), current_stream);
} else if (music != nullptr) {
UnloadMix(music);
music = LoadMix(file.c_str());
}
}
if (music || stream) {
playback_ready.store(true);
} else {
playback_ready.store(false);
}
}
if (flag_mutex.try_lock()) {
if (seeking.exchange(false)) {
if (stream != nullptr) {
SDL_LockAudioDevice(device);
seek_vgmstream(stream, (int32_t)((double)stream->sample_rate * position));
st->flush();
SDL_UnlockAudioDevice(device);
} else {
Mix_SetMusicPositionStream(music, position);
}
set_signal(PlaybackSignalSeeked);
}
if (pause_changed.exchange(false)) {
if (stream != nullptr) {
SDL_PauseAudioDevice(device, paused ? 1 : 0);
}
if (paused) {
if (music != nullptr) {
Mix_PauseMusicStream(music);
}
set_signal(PlaybackSignalPaused);
} else {
if (music != nullptr) {
Mix_ResumeMusicStream(music);
}
set_signal(PlaybackSignalResumed);
}
}
if (update.exchange(false)) {
SDL_LockAudioDevice(device);
real_volume = volume / 100.0;
if (stream == nullptr) {
Mix_VolumeMusicStream(music, (int)(volume / 100.0 * MIX_MAX_VOLUME));
}
UpdateST();
size_t correct_buf_size = CalculateBufSize(&spec, GetMaxSeconds(), MaxSeconds);
size_t max_buf_size = correct_buf_size * 10;
bool too_large = max_buf_size < bufsize;
bool too_small = correct_buf_size > bufsize;
if (too_large) {
ERROR.writes("Bufsize is too large - ");
} else if (too_small) {
ERROR.writes("Bufsize is too small - ");
}
if (too_large || too_small) {
ERROR.writeln("Resizing buffer...");
general_mixer = nullptr;
bufsize = 0;
buf = (Uint8*)realloc((void*)buf, correct_buf_size);
if (buf == nullptr) {
ERROR.writes("Failed to allocate memory for playback!");
set_error("Failed to allocate memory for playback!");
set_signal(PlaybackSignalErrorOccurred);
running = false;
stop_loop();
return;
}
bufsize = correct_buf_size;
}
SDL_UnlockAudioDevice(device);
}
flag_mutex.unlock();
}
if (stream != nullptr) {
double maybe_new_position = (double)stream->current_sample / stream->sample_rate;
if (position > maybe_new_position) {
position = maybe_new_position;
}
position += 0.02 * (speed * tempo);
} else if (music != nullptr) {
position = Mix_GetMusicPosition(music);
}
}
void PlaybackInstance::DeinitLoopFunction() {
playback_ready.store(false);
// ====
if (music != nullptr) {
UnloadMix(music);
}
if (stream != nullptr) {
UnloadVgm(stream);
}
SDL_CloseAudioDevice(device);
Mix_CloseAudio();
Mix_Quit();
SDL_QuitSubSystem(SDL_INIT_AUDIO);
delete st;
free(buf);
current_file_mutex.lock(); current_file_mutex.lock();
current_file = {}; current_file = {};
current_file_mutex.unlock(); current_file_mutex.unlock();
set_signal(PlaybackSignalStopped); 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() { PlaybackInstance::PlaybackInstance() {
running = false; running = false;
@ -546,7 +566,11 @@ void PlaybackInstance::Load(std::string filePath) {
if (running.exchange(true)) { if (running.exchange(true)) {
load_requested.store(true); load_requested.store(true);
} else { } else {
#ifdef __EMSCRIPTEN__
start_loop();
#else
thread = std::thread(&PlaybackInstance::ThreadFunc, this); thread = std::thread(&PlaybackInstance::ThreadFunc, this);
#endif
} }
flag_mutex.lock(); flag_mutex.lock();
this->position = 0.0; this->position = 0.0;
@ -557,9 +581,15 @@ void PlaybackInstance::Load(std::string filePath) {
} }
void PlaybackInstance::Start(std::string filePath, int streamIdx) { void PlaybackInstance::Start(std::string filePath, int streamIdx) {
Load(filePath); Load(filePath);
while (!load_finished.exchange(false)) { while (loop_started && !load_finished.exchange(false)) {
#ifdef __EMSCRIPTEN__
LoopHook();
#endif
std::this_thread::sleep_for(20ms); std::this_thread::sleep_for(20ms);
} }
if (!loop_started) {
return;
}
INFO.writefln("Playing %s...", filePath.c_str()); INFO.writefln("Playing %s...", filePath.c_str());
flag_mutex.lock(); flag_mutex.lock();
this->position = 0.0; this->position = 0.0;
@ -607,7 +637,11 @@ bool PlaybackInstance::IsPaused() {
void PlaybackInstance::Stop() { void PlaybackInstance::Stop() {
if (running.exchange(false)) { if (running.exchange(false)) {
#ifdef __EMSCRIPTEN__
stop_loop();
#else
thread.join(); thread.join();
#endif
} }
} }
void PlaybackInstance::Update() { void PlaybackInstance::Update() {
@ -713,6 +747,7 @@ void Playback::set_error(std::string desc) {
set_signal(PlaybackSignalErrorOccurred); set_signal(PlaybackSignalErrorOccurred);
} }
Playback *Playback::Create(bool *daemon_found, bool daemon) { Playback *Playback::Create(bool *daemon_found, bool daemon) {
#ifdef DBUS_ENABLED
auto *dbus_proxy = DBusAPISender::Create(); auto *dbus_proxy = DBusAPISender::Create();
if (dbus_proxy != nullptr) { if (dbus_proxy != nullptr) {
if (daemon_found != nullptr) { if (daemon_found != nullptr) {
@ -729,6 +764,7 @@ Playback *Playback::Create(bool *daemon_found, bool daemon) {
if (daemon_found != nullptr) { if (daemon_found != nullptr) {
*daemon_found = false; *daemon_found = false;
} }
#endif
DEBUG.writeln("Creating new playback instance."); DEBUG.writeln("Creating new playback instance.");
return new PlaybackInstance(); return new PlaybackInstance();
} }

View file

@ -185,6 +185,30 @@ class Playback {
Pause(); 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); static Playback *Create(bool *daemon_found, bool daemon = false);
}; };
class DBusAPISender; class DBusAPISender;
@ -227,6 +251,7 @@ private:
void UnloadMix(Mix_Music* music); void UnloadMix(Mix_Music* music);
void UnloadVgm(VGMSTREAM *stream); void UnloadVgm(VGMSTREAM *stream);
VGMSTREAM *stream; VGMSTREAM *stream;
Mix_Music *music;
std::vector<PlaybackStream> streams; std::vector<PlaybackStream> streams;
std::mutex stream_list_mutex; std::mutex stream_list_mutex;
double real_volume = 1.0; double real_volume = 1.0;
@ -239,7 +264,6 @@ private:
std::optional<std::string> current_file; std::optional<std::string> current_file;
std::optional<std::string> current_title; std::optional<std::string> current_title;
float prev_pitch, prev_speed, prev_tempo; float prev_pitch, prev_speed, prev_tempo;
public: public:
PlaybackInstance(); PlaybackInstance();
~PlaybackInstance() override; ~PlaybackInstance() override;
@ -269,6 +293,9 @@ public:
float GetPitch() override; float GetPitch() override;
float GetSpeed() override; float GetSpeed() override;
float GetVolume() override; float GetVolume() override;
void InitLoopFunction() override;
void DeinitLoopFunction() override;
void LoopFunction() override;
float volume; float volume;
float speed; float speed;
float tempo; float tempo;

View file

@ -1,3 +1,4 @@
#ifdef DBUS_ENABLED
#include "proxy_backend.hpp" #include "proxy_backend.hpp"
#include "log.hpp" #include "log.hpp"
#include <thread> #include <thread>
@ -30,3 +31,4 @@ int ProxyGlueBackend::run(std::vector<std::string> realArgs, int argc, char **ar
void ProxyGlueBackend::quitDaemon() { void ProxyGlueBackend::quitDaemon() {
((DBusAPISender*)playback)->Quit(); ((DBusAPISender*)playback)->Quit();
} }
#endif

View file

@ -1,4 +1,5 @@
#pragma once #pragma once
#ifdef DBUS_ENABLED
#include "backend.hpp" #include "backend.hpp"
class ProxyGlueBackend : public UIBackend { class ProxyGlueBackend : public UIBackend {
DBusAPISender *sender; DBusAPISender *sender;
@ -9,3 +10,4 @@ class ProxyGlueBackend : public UIBackend {
void quitDaemon(); void quitDaemon();
int run(std::vector<std::string> realArgs, int argc, char **argv) override; int run(std::vector<std::string> realArgs, int argc, char **argv) override;
}; };
#endif

@ -0,0 +1 @@
Subproject commit e83424d5928ab8513d2d082779c275765dee31b9

55
web/api.js Normal file
View 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();
}
})

View 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)

View 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
View 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, "&amp;");
//text = text.replace(/</g, "&lt;");
//text = text.replace(/>/g, "&gt;");
//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
View 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()