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

View file

@ -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")

View file

@ -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() {

View file

@ -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();
};

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

View file

@ -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;

View file

@ -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) {

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
};

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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;

View file

@ -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

View file

@ -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

@ -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()