From 7534fa10230483ae47e94c4e0a89e85f13d19dc5 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 16 Mar 2024 09:04:29 -0400 Subject: [PATCH 01/41] refactor(video): move encoder declarations to header (#2185) --- src/video.cpp | 157 ++------------------------------------------- src/video.h | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 150 deletions(-) diff --git a/src/video.cpp b/src/video.cpp index f786aeb59f0..920ce1a4857 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -14,7 +14,6 @@ extern "C" { #include #include #include -#include } #include "cbs.h" @@ -51,12 +50,6 @@ namespace video { av_buffer_unref(&ref); } - using avcodec_ctx_t = util::safe_ptr; - using avcodec_frame_t = util::safe_ptr; - using avcodec_buffer_t = util::safe_ptr; - using sws_t = util::safe_ptr; - using img_event_t = std::shared_ptr>>; - namespace nv { enum class profile_h264_e : int { @@ -87,11 +80,6 @@ namespace video { }; } // namespace qsv - platf::mem_type_e - map_base_dev_type(AVHWDeviceType type); - platf::pix_fmt_e - map_pix_fmt(AVPixelFormat fmt); - util::Either dxgi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *); util::Either @@ -288,137 +276,6 @@ namespace video { ALWAYS_REPROBE = 1 << 9, // This is an encoder of last resort and we want to aggressively probe for a better one }; - struct encoder_platform_formats_t { - virtual ~encoder_platform_formats_t() = default; - platf::mem_type_e dev_type; - platf::pix_fmt_e pix_fmt_8bit, pix_fmt_10bit; - }; - - struct encoder_platform_formats_avcodec: encoder_platform_formats_t { - using init_buffer_function_t = std::function(platf::avcodec_encode_device_t *)>; - - encoder_platform_formats_avcodec( - const AVHWDeviceType &avcodec_base_dev_type, - const AVHWDeviceType &avcodec_derived_dev_type, - const AVPixelFormat &avcodec_dev_pix_fmt, - const AVPixelFormat &avcodec_pix_fmt_8bit, - const AVPixelFormat &avcodec_pix_fmt_10bit, - const init_buffer_function_t &init_avcodec_hardware_input_buffer_function): - avcodec_base_dev_type { avcodec_base_dev_type }, - avcodec_derived_dev_type { avcodec_derived_dev_type }, - avcodec_dev_pix_fmt { avcodec_dev_pix_fmt }, - avcodec_pix_fmt_8bit { avcodec_pix_fmt_8bit }, - avcodec_pix_fmt_10bit { avcodec_pix_fmt_10bit }, - init_avcodec_hardware_input_buffer { init_avcodec_hardware_input_buffer_function } { - dev_type = map_base_dev_type(avcodec_base_dev_type); - pix_fmt_8bit = map_pix_fmt(avcodec_pix_fmt_8bit); - pix_fmt_10bit = map_pix_fmt(avcodec_pix_fmt_10bit); - } - - AVHWDeviceType avcodec_base_dev_type, avcodec_derived_dev_type; - AVPixelFormat avcodec_dev_pix_fmt; - AVPixelFormat avcodec_pix_fmt_8bit, avcodec_pix_fmt_10bit; - - init_buffer_function_t init_avcodec_hardware_input_buffer; - }; - - struct encoder_platform_formats_nvenc: encoder_platform_formats_t { - encoder_platform_formats_nvenc( - const platf::mem_type_e &dev_type, - const platf::pix_fmt_e &pix_fmt_8bit, - const platf::pix_fmt_e &pix_fmt_10bit) { - encoder_platform_formats_t::dev_type = dev_type; - encoder_platform_formats_t::pix_fmt_8bit = pix_fmt_8bit; - encoder_platform_formats_t::pix_fmt_10bit = pix_fmt_10bit; - } - }; - - struct encoder_t { - std::string_view name; - enum flag_e { - PASSED, // Is supported - REF_FRAMES_RESTRICT, // Set maximum reference frames - CBR, // Some encoders don't support CBR, if not supported --> attempt constant quantatication parameter instead - DYNAMIC_RANGE, // hdr - VUI_PARAMETERS, // AMD encoder with VAAPI doesn't add VUI parameters to SPS - MAX_FLAGS - }; - - static std::string_view - from_flag(flag_e flag) { -#define _CONVERT(x) \ - case flag_e::x: \ - return #x##sv - switch (flag) { - _CONVERT(PASSED); - _CONVERT(REF_FRAMES_RESTRICT); - _CONVERT(CBR); - _CONVERT(DYNAMIC_RANGE); - _CONVERT(VUI_PARAMETERS); - _CONVERT(MAX_FLAGS); - } -#undef _CONVERT - - return "unknown"sv; - } - - struct option_t { - KITTY_DEFAULT_CONSTR_MOVE(option_t) - option_t(const option_t &) = default; - - std::string name; - std::variant *, std::function, std::string, std::string *> value; - - option_t(std::string &&name, decltype(value) &&value): - name { std::move(name) }, value { std::move(value) } {} - }; - - const std::unique_ptr platform_formats; - - struct { - std::vector common_options; - std::vector sdr_options; - std::vector hdr_options; - std::vector fallback_options; - - // QP option to set in the case that CBR/VBR is not supported - // by the encoder. If CBR/VBR is guaranteed to be supported, - // don't specify this option to avoid wasteful encoder probing. - std::optional qp; - - std::string name; - std::bitset capabilities; - - bool - operator[](flag_e flag) const { - return capabilities[(std::size_t) flag]; - } - - std::bitset::reference - operator[](flag_e flag) { - return capabilities[(std::size_t) flag]; - } - } av1, hevc, h264; - - uint32_t flags; - }; - - struct encode_session_t { - virtual ~encode_session_t() = default; - - virtual int - convert(platf::img_t &img) = 0; - - virtual void - request_idr_frame() = 0; - - virtual void - request_normal_frame() = 0; - - virtual void - invalidate_ref_frames(int64_t first_frame, int64_t last_frame) = 0; - }; - class avcodec_encode_session_t: public encode_session_t { public: avcodec_encode_session_t() = default; @@ -586,7 +443,7 @@ namespace video { auto capture_thread_sync = safe::make_shared(start_capture_sync, end_capture_sync); #ifdef _WIN32 - static encoder_t nvenc { + encoder_t nvenc { "nvenc"sv, std::make_unique( platf::mem_type_e::dxgi, @@ -630,7 +487,7 @@ namespace video { PARALLEL_ENCODING | REF_FRAMES_INVALIDATION // flags }; #elif !defined(__APPLE__) - static encoder_t nvenc { + encoder_t nvenc { "nvenc"sv, std::make_unique( #ifdef _WIN32 @@ -718,7 +575,7 @@ namespace video { #endif #ifdef _WIN32 - static encoder_t quicksync { + encoder_t quicksync { "quicksync"sv, std::make_unique( AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_QSV, @@ -799,7 +656,7 @@ namespace video { PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT }; - static encoder_t amdvce { + encoder_t amdvce { "amdvce"sv, std::make_unique( AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE, @@ -871,7 +728,7 @@ namespace video { }; #endif - static encoder_t software { + encoder_t software { "software"sv, std::make_unique( AV_HWDEVICE_TYPE_NONE, AV_HWDEVICE_TYPE_NONE, @@ -936,7 +793,7 @@ namespace video { }; #ifdef __linux__ - static encoder_t vaapi { + encoder_t vaapi { "vaapi"sv, std::make_unique( AV_HWDEVICE_TYPE_VAAPI, AV_HWDEVICE_TYPE_NONE, @@ -1004,7 +861,7 @@ namespace video { #endif #ifdef __APPLE__ - static encoder_t videotoolbox { + encoder_t videotoolbox { "videotoolbox"sv, std::make_unique( AV_HWDEVICE_TYPE_VIDEOTOOLBOX, AV_HWDEVICE_TYPE_NONE, diff --git a/src/video.h b/src/video.h index fec5c38b343..eb8eabc358d 100644 --- a/src/video.h +++ b/src/video.h @@ -11,11 +11,181 @@ extern "C" { #include +#include } struct AVPacket; namespace video { + platf::mem_type_e + map_base_dev_type(AVHWDeviceType type); + platf::pix_fmt_e + map_pix_fmt(AVPixelFormat fmt); + + void + free_ctx(AVCodecContext *ctx); + void + free_frame(AVFrame *frame); + void + free_buffer(AVBufferRef *ref); + + using avcodec_ctx_t = util::safe_ptr; + using avcodec_frame_t = util::safe_ptr; + using avcodec_buffer_t = util::safe_ptr; + using sws_t = util::safe_ptr; + using img_event_t = std::shared_ptr>>; + + struct encoder_platform_formats_t { + virtual ~encoder_platform_formats_t() = default; + platf::mem_type_e dev_type; + platf::pix_fmt_e pix_fmt_8bit, pix_fmt_10bit; + }; + + struct encoder_platform_formats_avcodec: encoder_platform_formats_t { + using init_buffer_function_t = std::function(platf::avcodec_encode_device_t *)>; + + encoder_platform_formats_avcodec( + const AVHWDeviceType &avcodec_base_dev_type, + const AVHWDeviceType &avcodec_derived_dev_type, + const AVPixelFormat &avcodec_dev_pix_fmt, + const AVPixelFormat &avcodec_pix_fmt_8bit, + const AVPixelFormat &avcodec_pix_fmt_10bit, + const init_buffer_function_t &init_avcodec_hardware_input_buffer_function): + avcodec_base_dev_type { avcodec_base_dev_type }, + avcodec_derived_dev_type { avcodec_derived_dev_type }, + avcodec_dev_pix_fmt { avcodec_dev_pix_fmt }, + avcodec_pix_fmt_8bit { avcodec_pix_fmt_8bit }, + avcodec_pix_fmt_10bit { avcodec_pix_fmt_10bit }, + init_avcodec_hardware_input_buffer { init_avcodec_hardware_input_buffer_function } { + dev_type = map_base_dev_type(avcodec_base_dev_type); + pix_fmt_8bit = map_pix_fmt(avcodec_pix_fmt_8bit); + pix_fmt_10bit = map_pix_fmt(avcodec_pix_fmt_10bit); + } + + AVHWDeviceType avcodec_base_dev_type, avcodec_derived_dev_type; + AVPixelFormat avcodec_dev_pix_fmt; + AVPixelFormat avcodec_pix_fmt_8bit, avcodec_pix_fmt_10bit; + + init_buffer_function_t init_avcodec_hardware_input_buffer; + }; + + struct encoder_platform_formats_nvenc: encoder_platform_formats_t { + encoder_platform_formats_nvenc( + const platf::mem_type_e &dev_type, + const platf::pix_fmt_e &pix_fmt_8bit, + const platf::pix_fmt_e &pix_fmt_10bit) { + encoder_platform_formats_t::dev_type = dev_type; + encoder_platform_formats_t::pix_fmt_8bit = pix_fmt_8bit; + encoder_platform_formats_t::pix_fmt_10bit = pix_fmt_10bit; + } + }; + + struct encoder_t { + std::string_view name; + enum flag_e { + PASSED, // Is supported + REF_FRAMES_RESTRICT, // Set maximum reference frames + CBR, // Some encoders don't support CBR, if not supported --> attempt constant quantatication parameter instead + DYNAMIC_RANGE, // hdr + VUI_PARAMETERS, // AMD encoder with VAAPI doesn't add VUI parameters to SPS + MAX_FLAGS + }; + + static std::string_view + from_flag(flag_e flag) { +#define _CONVERT(x) \ + case flag_e::x: \ + std::string_view(#x) + switch (flag) { + _CONVERT(PASSED); + _CONVERT(REF_FRAMES_RESTRICT); + _CONVERT(CBR); + _CONVERT(DYNAMIC_RANGE); + _CONVERT(VUI_PARAMETERS); + _CONVERT(MAX_FLAGS); + } +#undef _CONVERT + + return { "unknown" }; + } + + struct option_t { + KITTY_DEFAULT_CONSTR_MOVE(option_t) + option_t(const option_t &) = default; + + std::string name; + std::variant *, std::function, std::string, std::string *> value; + + option_t(std::string &&name, decltype(value) &&value): + name { std::move(name) }, value { std::move(value) } {} + }; + + const std::unique_ptr platform_formats; + + struct codec_t { + std::vector common_options; + std::vector sdr_options; + std::vector hdr_options; + std::vector fallback_options; + + // QP option to set in the case that CBR/VBR is not supported + // by the encoder. If CBR/VBR is guaranteed to be supported, + // don't specify this option to avoid wasteful encoder probing. + std::optional qp; + + std::string name; + std::bitset capabilities; + + bool + operator[](flag_e flag) const { + return capabilities[(std::size_t) flag]; + } + + std::bitset::reference + operator[](flag_e flag) { + return capabilities[(std::size_t) flag]; + } + } av1, hevc, h264; + + uint32_t flags; + }; + + struct encode_session_t { + virtual ~encode_session_t() = default; + + virtual int + convert(platf::img_t &img) = 0; + + virtual void + request_idr_frame() = 0; + + virtual void + request_normal_frame() = 0; + + virtual void + invalidate_ref_frames(int64_t first_frame, int64_t last_frame) = 0; + }; + + // encoders + extern encoder_t software; + +#if !defined(__APPLE__) + extern encoder_t nvenc; // available for windows and linux +#endif + +#ifdef _WIN32 + extern encoder_t amdvce; + extern encoder_t quicksync; +#endif + +#ifdef __linux__ + extern encoder_t vaapi; +#endif + +#ifdef __APPLE__ + extern encoder_t videotoolbox; +#endif + struct packet_raw_t { virtual ~packet_raw_t() = default; @@ -154,6 +324,8 @@ namespace video { config_t config, void *channel_data); + bool + validate_encoder(encoder_t &encoder, bool expect_failure); int probe_encoders(); } // namespace video From 8316f44e10f3f6ef27fc27b3ef6e69f359b1b667 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sun, 17 Mar 2024 00:07:18 -0400 Subject: [PATCH 02/41] ci(linux): refactor linux build (#2275) --- .github/workflows/CI.yml | 108 +++++++++++++++------------------------ 1 file changed, 40 insertions(+), 68 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 09a0aaeae66..a35f2affbcc 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -165,7 +165,7 @@ jobs: remove-android: 'true' remove-haskell: 'true' remove-codeql: 'true' - remove-docker-images: 'false' + remove-docker-images: 'true' - name: Checkout uses: actions/checkout@v4 @@ -291,61 +291,48 @@ jobs: remove-android: 'true' remove-haskell: 'true' remove-codeql: 'true' - remove-docker-images: 'false' + remove-docker-images: 'true' - name: Checkout uses: actions/checkout@v4 with: submodules: recursive + - name: Install wget + run: | + sudo apt-get update -y + sudo apt-get install -y \ + wget + + - name: Install CUDA + env: + CUDA_VERSION: 11.8.0 + CUDA_BUILD: 520.61.05 + timeout-minutes: 4 + run: | + url_base="https://developer.download.nvidia.com/compute/cuda/${CUDA_VERSION}/local_installers" + url="${url_base}/cuda_${CUDA_VERSION}_${CUDA_BUILD}_linux.run" + sudo wget -q -O /root/cuda.run ${url} + sudo chmod a+x /root/cuda.run + sudo /root/cuda.run --silent --toolkit --toolkitpath=/usr/local/cuda --no-opengl-libs --no-man-page --no-drm + sudo rm /root/cuda.run + - name: Setup Dependencies Linux run: | + # allow newer gcc sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y - if [[ ${{ matrix.dist }} == "18.04" ]]; then - # Ubuntu 18.04 packages - sudo add-apt-repository ppa:savoury1/boost-defaults-1.71 -y - - sudo apt-get update -y - sudo apt-get install -y \ - libboost-filesystem1.71-dev \ - libboost-locale1.71-dev \ - libboost-log1.71-dev \ - libboost-regex1.71-dev \ - libboost-program-options1.71-dev - - # Install cmake - wget https://cmake.org/files/v3.22/cmake-3.22.2-linux-x86_64.sh - chmod +x cmake-3.22.2-linux-x86_64.sh - mkdir /opt/cmake - ./cmake-3.22.2-linux-x86_64.sh --prefix=/opt/cmake --skip-license - ln --force --symbolic /opt/cmake/bin/cmake /usr/local/bin/cmake - cmake --version - - # install newer tar from focal... appimagelint fails on 18.04 without this - echo "original tar version" - tar --version - wget -O tar.deb http://security.ubuntu.com/ubuntu/pool/main/t/tar/tar_1.30+dfsg-7ubuntu0.20.04.3_amd64.deb - sudo apt-get -y install -f ./tar.deb - echo "new tar version" - tar --version - else - # Ubuntu 20.04+ packages - sudo apt-get update -y - sudo apt-get install -y \ - cmake \ - libboost-filesystem-dev \ - libboost-locale-dev \ - libboost-log-dev \ - libboost-program-options-dev - fi - sudo apt-get install -y \ build-essential \ + cmake \ gcc-10 \ g++-10 \ libayatana-appindicator3-dev \ libavdevice-dev \ + libboost-filesystem-dev \ + libboost-locale-dev \ + libboost-log-dev \ + libboost-program-options-dev \ libcap-dev \ libcurl4-openssl-dev \ libdrm-dev \ @@ -366,8 +353,7 @@ jobs: libxcb1-dev \ libxfixes-dev \ libxrandr-dev \ - libxtst-dev \ - wget + libxtst-dev # clean apt cache sudo apt-get clean @@ -382,20 +368,15 @@ jobs: --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-10 \ --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-10 - # Install CUDA - sudo wget \ - https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.run \ - --progress=bar:force:noscroll -q --show-progress -O /root/cuda.run - sudo chmod a+x /root/cuda.run - sudo /root/cuda.run --silent --toolkit --toolkitpath=/usr --no-opengl-libs --no-man-page --no-drm - sudo rm /root/cuda.run - - name: Build Linux env: BRANCH: ${{ github.head_ref || github.ref_name }} BUILD_VERSION: ${{ needs.check_changelog.outputs.next_version_bare }} COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + timeout-minutes: 5 run: | + echo "nproc: $(nproc)" + mkdir -p build mkdir -p artifacts @@ -403,6 +384,7 @@ jobs: cmake \ -DBUILD_WERROR=ON \ -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CUDA_COMPILER:PATH=/usr/local/cuda/bin/nvcc \ -DCMAKE_INSTALL_PREFIX=/usr \ -DSUNSHINE_ASSETS_DIR=share/sunshine \ -DSUNSHINE_EXECUTABLE_PATH=/usr/bin/sunshine \ @@ -412,20 +394,7 @@ jobs: -DSUNSHINE_ENABLE_CUDA=ON \ ${{ matrix.EXTRA_ARGS }} \ .. - make -j ${nproc} - - - name: Package Linux - CPACK - # todo - this is no longer used - if: ${{ matrix.type == 'cpack' }} - working-directory: build - run: | - cpack -G DEB - mv ./cpack_artifacts/Sunshine.deb ../artifacts/sunshine-${{ matrix.dist }}.deb - - if [[ ${{ matrix.dist }} == "20.04" ]]; then - cpack -G RPM - mv ./cpack_artifacts/Sunshine.rpm ../artifacts/sunshine.rpm - fi + make -j $(expr $(nproc) - 1) # use all but one core - name: Set AppImage Version if: | @@ -452,12 +421,12 @@ jobs: # AppImage # https://docs.appimage.org/packaging-guide/index.html - wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage chmod +x linuxdeploy-x86_64.AppImage # https://github.com/linuxdeploy/linuxdeploy-plugin-gtk sudo apt-get install libgtk-3-dev librsvg2-dev -y - wget https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh + wget -q https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh chmod +x linuxdeploy-plugin-gtk.sh export DEPLOY_GTK_VERSION=3 @@ -475,14 +444,17 @@ jobs: # permissions chmod +x ../artifacts/sunshine.AppImage + - name: Delete cuda + # free up space on the runner + run: | + sudo rm -rf /usr/local/cuda + - name: Verify AppImage if: ${{ matrix.type == 'AppImage' }} run: | wget https://github.com/TheAssassin/appimagelint/releases/download/continuous/appimagelint-x86_64.AppImage chmod +x appimagelint-x86_64.AppImage - # rm -rf ~/.cache/appimagelint/ - ./appimagelint-x86_64.AppImage ./artifacts/sunshine.AppImage - name: Upload Artifacts From 87774333f38a354a03aa9dceeba74593beec2e2f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 22 Mar 2024 19:54:12 -0400 Subject: [PATCH 03/41] feat(i18n): add ui localization (#2279) Co-authored-by: TheElixZammuto <6505622+TheElixZammuto@users.noreply.github.com> --- crowdin.yml | 12 +- docs/source/about/advanced_usage.rst | 34 + docs/source/contributing/localization.rst | 84 +- package.json | 3 +- src/config.cpp | 14 + src/config.h | 1 + src/confighttp.cpp | 19 + src_assets/common/assets/web/Navbar.vue | 12 +- src_assets/common/assets/web/ResourceCard.vue | 21 +- src_assets/common/assets/web/apps.html | 178 ++-- src_assets/common/assets/web/config.html | 818 +++++++----------- src_assets/common/assets/web/index.html | 35 +- src_assets/common/assets/web/locale.js | 27 + src_assets/common/assets/web/password.html | 34 +- src_assets/common/assets/web/pin.html | 23 +- .../assets/web/public/assets/css/sunshine.css | 4 + .../assets/web/public/assets/locale/de.json | 376 ++++++++ .../web/public/assets/locale/en-GB.json | 376 ++++++++ .../web/public/assets/locale/en-US.json | 376 ++++++++ .../assets/web/public/assets/locale/en.json | 376 ++++++++ .../assets/web/public/assets/locale/es.json | 376 ++++++++ .../assets/web/public/assets/locale/fr.json | 376 ++++++++ .../assets/web/public/assets/locale/it.json | 376 ++++++++ .../assets/web/public/assets/locale/ru.json | 376 ++++++++ .../assets/web/public/assets/locale/sv.json | 376 ++++++++ .../assets/web/public/assets/locale/zh.json | 376 ++++++++ .../common/assets/web/template_header.html | 1 + .../common/assets/web/troubleshooting.html | 52 +- src_assets/common/assets/web/welcome.html | 33 +- 29 files changed, 4446 insertions(+), 719 deletions(-) create mode 100644 src_assets/common/assets/web/locale.js create mode 100644 src_assets/common/assets/web/public/assets/css/sunshine.css create mode 100644 src_assets/common/assets/web/public/assets/locale/de.json create mode 100644 src_assets/common/assets/web/public/assets/locale/en-GB.json create mode 100644 src_assets/common/assets/web/public/assets/locale/en-US.json create mode 100644 src_assets/common/assets/web/public/assets/locale/en.json create mode 100644 src_assets/common/assets/web/public/assets/locale/es.json create mode 100644 src_assets/common/assets/web/public/assets/locale/fr.json create mode 100644 src_assets/common/assets/web/public/assets/locale/it.json create mode 100644 src_assets/common/assets/web/public/assets/locale/ru.json create mode 100644 src_assets/common/assets/web/public/assets/locale/sv.json create mode 100644 src_assets/common/assets/web/public/assets/locale/zh.json diff --git a/crowdin.yml b/crowdin.yml index 0be504ba7d8..3dd19366ef0 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,7 +1,7 @@ --- "base_path": "." "base_url": "https://api.crowdin.com" # optional (for Crowdin Enterprise only) -"preserve_hierarchy": false # flatten tree on crowdin +"preserve_hierarchy": true # false will flatten tree on crowdin, but doesn't work with dest option "pull_request_labels": [ "crowdin", "l10n" @@ -10,6 +10,7 @@ "files": [ { "source": "/locale/*.po", + "dest": "/%original_file_name%", "translation": "/locale/%two_letters_code%/LC_MESSAGES/%original_file_name%", "languages_mapping": { "two_letters_code": { @@ -17,6 +18,13 @@ "en-GB": "en_GB", "en-US": "en_US" } - } + }, + "update_option": "update_as_unapproved" + }, + { + "source": "/src_assets/common/assets/web/public/assets/locale/en.json", + "dest": "/sunshine.json", + "translation": "/src_assets/common/assets/web/public/assets/locale/%two_letters_code%.%file_extension%", + "update_option": "update_as_unapproved" } ] diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index f29e6d05f8d..d23b96cc582 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -47,6 +47,40 @@ editing the `conf` file in a text editor. Use the examples as reference. `General `__ ----------------------------------------------------- +`locale `__ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + The locale used for Sunshine's user interface. + +**Choices** + +.. table:: + :widths: auto + + ======= =========== + Value Description + ======= =========== + de German + en English + en-GB English (UK) + en-US English (United States) + es Spanish + fr French + it Italian + ru Russian + sv Swedish + zh Chinese (Simplified) + ======= =========== + +**Default** + ``en`` + +**Example** + .. code-block:: text + + locale = en + `sunshine_name `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/contributing/localization.rst b/docs/source/contributing/localization.rst index 2ca912805e1..0148b8170e4 100644 --- a/docs/source/contributing/localization.rst +++ b/docs/source/contributing/localization.rst @@ -30,42 +30,74 @@ localization there. Extraction ---------- -There should be minimal cases where strings need to be extracted from source code; however it may be necessary in some -situations. For example if a system tray icon is added it should be localized as it is user interfacing. -- Wrap the string to be extracted in a function as shown. - .. code-block:: cpp +.. tab:: UI - #include - #include + Sunshine uses `Vue I18n `__ for localizing the UI. + The following is a simple example of how to use it. - std::string msg = boost::locale::translate("Hello world!"); + - Add the string to `src_assets/common/assets/web/public/assets/locale/en.json`, in English. + .. code-block:: json -.. tip:: More examples can be found in the documentation for - `boost locale `__. + { + "index": { + "welcome": "Hello, Sunshine!" + } + } -.. warning:: This is for information only. Contributors should never include manually updated template files, or - manually compiled language files in Pull Requests. + .. note:: The json keys should be sorted alphabetically. You can use `jsonabc `__ + to sort the keys. -Strings are automatically extracted from the code to the `locale/sunshine.po` template file. The generated file is -used by CrowdIn to generate language specific template files. The file is generated using the -`.github/workflows/localize.yml` workflow and is run on any push event into the `nightly` branch. Jobs are only run if -any of the following paths are modified. + - Use the string in a Vue component. + .. code-block:: html -.. code-block:: yaml + - - 'src/**' + .. tip:: More formatting examples can be found in the + `Vue I18n guide `__. -When testing locally it may be desirable to manually extract, initialize, update, and compile strings. Python is -required for this, along with the python dependencies in the `./scripts/requirements.txt` file. Additionally, -`xgettext `__ must be installed. +.. tab:: C++ -**Extract, initialize, and update** - .. code-block:: bash + There should be minimal cases where strings need to be extracted from C++ source code; however it may be necessary in + some situations. For example the system tray icon could be localized as it is user interfacing. - python ./scripts/_locale.py --extract --init --update + - Wrap the string to be extracted in a function as shown. + .. code-block:: cpp -**Compile** - .. code-block:: bash + #include + #include - python ./scripts/_locale.py --compile + std::string msg = boost::locale::translate("Hello world!"); + + .. tip:: More examples can be found in the documentation for + `boost locale `__. + + .. warning:: This is for information only. Contributors should never include manually updated template files, or + manually compiled language files in Pull Requests. + + Strings are automatically extracted from the code to the `locale/sunshine.po` template file. The generated file is + used by CrowdIn to generate language specific template files. The file is generated using the + `.github/workflows/localize.yml` workflow and is run on any push event into the `nightly` branch. Jobs are only run if + any of the following paths are modified. + + .. code-block:: yaml + + - 'src/**' + + When testing locally it may be desirable to manually extract, initialize, update, and compile strings. Python is + required for this, along with the python dependencies in the `./scripts/requirements.txt` file. Additionally, + `xgettext `__ must be installed. + + **Extract, initialize, and update** + .. code-block:: bash + + python ./scripts/_locale.py --extract --init --update + + **Compile** + .. code-block:: bash + + python ./scripts/_locale.py --compile diff --git a/package.json b/package.json index b685e0966a4..706c23384ce 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "bootstrap": "5.3.3", "vite": "4.5.2", "vite-plugin-ejs": "1.6.4", - "vue": "3.4.5" + "vue": "3.4.5", + "vue-i18n": "9.10.2" } } diff --git a/src/config.cpp b/src/config.cpp index 21562f83487..b2660f7d2c1 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -441,6 +441,7 @@ namespace config { }; sunshine_t sunshine { + "en", // locale 2, // min_log_level 0, // flags {}, // User file @@ -1101,6 +1102,19 @@ namespace config { config::sunshine.flags[config::flag::UPNP].flip(); } + string_restricted_f(vars, "locale", config::sunshine.locale, { + "de"sv, // German + "en"sv, // English + "en-GB"sv, // English (UK) + "en-US"sv, // English (US) + "es"sv, // Spanish + "fr"sv, // French + "it"sv, // Italian + "ru"sv, // Russian + "sv"sv, // Swedish + "zh"sv, // Chinese + }); + std::string log_level_string; string_f(vars, "min_log_level", log_level_string); diff --git a/src/config.h b/src/config.h index 6c48f466b8e..f30e7e6a05a 100644 --- a/src/config.h +++ b/src/config.h @@ -160,6 +160,7 @@ namespace config { bool elevated; }; struct sunshine_t { + std::string locale; int min_log_level; std::bitset flags; std::string credentials_file; diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 0657902dd16..de25bf0e7cf 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -550,6 +550,24 @@ namespace confighttp { } } + void + getLocale(resp_https_t response, req_https_t request) { + // we need to return the locale whether authenticated or not + + print_req(request); + + pt::ptree outputTree; + auto g = util::fail_guard([&]() { + std::ostringstream data; + + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + outputTree.put("status", "true"); + outputTree.put("locale", config::sunshine.locale); + } + void saveConfig(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) return; @@ -743,6 +761,7 @@ namespace confighttp { server.resource["^/api/apps$"]["POST"] = saveApp; server.resource["^/api/config$"]["GET"] = getConfig; server.resource["^/api/config$"]["POST"] = saveConfig; + server.resource["^/api/configLocale$"]["GET"] = getLocale; server.resource["^/api/restart$"]["POST"] = restart; server.resource["^/api/password$"]["POST"] = savePassword; server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; diff --git a/src_assets/common/assets/web/Navbar.vue b/src_assets/common/assets/web/Navbar.vue index 948fb4d7244..9e4e1be64f5 100644 --- a/src_assets/common/assets/web/Navbar.vue +++ b/src_assets/common/assets/web/Navbar.vue @@ -11,22 +11,22 @@ diff --git a/src_assets/common/assets/web/ResourceCard.vue b/src_assets/common/assets/web/ResourceCard.vue index e2481ca475a..aee837689cf 100644 --- a/src_assets/common/assets/web/ResourceCard.vue +++ b/src_assets/common/assets/web/ResourceCard.vue @@ -1,35 +1,32 @@