From 6d341002dca3a9a4eccdcf9caa1cec7c3febd97e Mon Sep 17 00:00:00 2001 From: Sauntor Date: Fri, 12 Jan 2024 11:08:36 +0800 Subject: [PATCH] v1.0.0 --- .gitignore | 74 ++ App/CMakeLists.txt | 229 +++++ App/Config.h.in | 27 + App/QiliApp.cpp | 107 +++ App/QiliApp.h | 28 + App/QiliApp.qrc | 6 + App/QiliAppGlobal.h | 28 + App/QiliApp_zh_CN.ts | 448 ++++++++++ App/QiliConnection.cpp | 83 ++ App/QiliConnection.h | 60 ++ App/QiliCookieJar.cpp | 59 ++ App/QiliCookieJar.h | 45 + App/QiliGlobal.h | 84 ++ App/QiliHttp.cpp | 184 ++++ App/QiliHttp.h | 72 ++ App/QiliLauncher2.ui | 347 ++++++++ App/QiliLogger.cpp | 117 +++ App/QiliLogger.h | 56 ++ App/QiliLogin.cpp | 229 +++++ App/QiliLogin.h | 82 ++ App/QiliLogin.ui | 426 +++++++++ App/QiliProtocol.cpp | 190 ++++ App/QiliProtocol.h | 82 ++ App/QiliSettings.cpp | 124 +++ App/QiliSettings.h | 68 ++ App/QiliSettingsDialog.cpp | 218 +++++ App/QiliSettingsDialog.h | 75 ++ App/QiliSettingsDialog.ui | 403 +++++++++ App/QiliSocket.cpp | 159 ++++ App/QiliSocket.h | 66 ++ App/QiliSpeaker.cpp | 211 +++++ App/QiliSpeaker.h | 66 ++ App/QiliSubtitleLogger.cpp | 66 ++ App/QiliSubtitleLogger.h | 48 + App/QiliSubtitleLogger.ui | 139 +++ App/QiliThanksDialog.cpp | 30 + App/QiliThanksDialog.h | 40 + App/QiliThanksDialog.ui | 324 +++++++ App/QiliTray.cpp | 348 ++++++++ App/QiliTray.h | 99 +++ App/Utility.cpp | 156 ++++ App/Utility.h | 230 +++++ App/images/alipay.png | Bin 0 -> 89740 bytes App/images/qili.ico | Bin 0 -> 19374 bytes App/images/wechat.png | Bin 0 -> 109227 bytes App/main.cpp | 93 ++ CMakeLists.txt | 187 ++++ Config.h.in | 27 + Designer/CMakeLists.txt | 51 ++ Designer/DesignerGlobal.h | 28 + Designer/QiliDesigner.cpp | 32 + Designer/QiliDesigner.h | 39 + Designer/QiliTextFieldDesigner.cpp | 116 +++ Designer/QiliTextFieldDesigner.h | 49 ++ Designer/QiliTitleBarDesigner.cpp | 120 +++ Designer/QiliTitleBarDesigner.h | 51 ++ FindWrapBrotli.cmake | 100 +++ IFW/config/controller.qs | 8 + IFW/config/general.xml | 26 + IFW/packages/me.sauntor.qili/meta/desktop.qs | 33 + IFW/packages/me.sauntor.qili/meta/package.xml | 12 + IFW/packages/me.sauntor.qili/meta/zh_CN.ts | 11 + LICENSE | 674 ++++++++++++++ Linux/deb/postinst | 1 + Linux/deb/postrm | 1 + Linux/me.sauntor.qili.desktop.in | 15 + Linux/rpm/postinst | 1 + Linux/rpm/postrm | 1 + QiliCPack.cmake | 51 ++ QiliIFW.cmake | 114 +++ QiliInstall.cmake | 33 + README.md | 137 +++ README_en.md | 139 +++ Thirdparty/CMakeLists.txt | 43 + Thirdparty/QiliQRCode.cpp | 48 + Thirdparty/QiliQRCode.h | 42 + Thirdparty/ThirdpartyExports.h | 26 + Thirdparty/qrcodegen.cpp | 830 ++++++++++++++++++ Thirdparty/qrcodegen.hpp | 549 ++++++++++++ Widgets/CMakeLists.txt | 63 ++ Widgets/QiliDialog.cpp | 112 +++ Widgets/QiliDialog.h | 49 ++ Widgets/QiliTextField.cpp | 129 +++ Widgets/QiliTextField.h | 77 ++ Widgets/QiliTitleBar.cpp | 50 ++ Widgets/QiliTitleBar.h | 53 ++ Widgets/QiliTitleBar.ui | 105 +++ Widgets/QiliWidgets.qrc | 14 + Widgets/QiliWidgets_zh_CN.ts | 19 + Widgets/WidgetsGlobal.h | 26 + Widgets/images/bili-logo.svg | 1 + Widgets/images/bili-user.svg | 1 + Widgets/images/clear.svg | 1 + Widgets/images/clear_on.svg | 1 + Widgets/images/close.svg | 1 + Widgets/images/close_on.svg | 1 + Widgets/images/help.svg | 1 + Widgets/images/qili.ico | Bin 0 -> 19374 bytes Widgets/images/qili.png | Bin 0 -> 10999 bytes Widgets/images/save.svg | 1 + Widgets/themes/light.css | 127 +++ changelog | 3 + 102 files changed, 10456 insertions(+) create mode 100644 .gitignore create mode 100644 App/CMakeLists.txt create mode 100644 App/Config.h.in create mode 100644 App/QiliApp.cpp create mode 100644 App/QiliApp.h create mode 100644 App/QiliApp.qrc create mode 100644 App/QiliAppGlobal.h create mode 100644 App/QiliApp_zh_CN.ts create mode 100644 App/QiliConnection.cpp create mode 100644 App/QiliConnection.h create mode 100644 App/QiliCookieJar.cpp create mode 100644 App/QiliCookieJar.h create mode 100644 App/QiliGlobal.h create mode 100644 App/QiliHttp.cpp create mode 100644 App/QiliHttp.h create mode 100644 App/QiliLauncher2.ui create mode 100644 App/QiliLogger.cpp create mode 100644 App/QiliLogger.h create mode 100644 App/QiliLogin.cpp create mode 100644 App/QiliLogin.h create mode 100644 App/QiliLogin.ui create mode 100644 App/QiliProtocol.cpp create mode 100644 App/QiliProtocol.h create mode 100644 App/QiliSettings.cpp create mode 100644 App/QiliSettings.h create mode 100644 App/QiliSettingsDialog.cpp create mode 100644 App/QiliSettingsDialog.h create mode 100644 App/QiliSettingsDialog.ui create mode 100644 App/QiliSocket.cpp create mode 100644 App/QiliSocket.h create mode 100644 App/QiliSpeaker.cpp create mode 100644 App/QiliSpeaker.h create mode 100644 App/QiliSubtitleLogger.cpp create mode 100644 App/QiliSubtitleLogger.h create mode 100644 App/QiliSubtitleLogger.ui create mode 100644 App/QiliThanksDialog.cpp create mode 100644 App/QiliThanksDialog.h create mode 100644 App/QiliThanksDialog.ui create mode 100644 App/QiliTray.cpp create mode 100644 App/QiliTray.h create mode 100644 App/Utility.cpp create mode 100644 App/Utility.h create mode 100644 App/images/alipay.png create mode 100644 App/images/qili.ico create mode 100644 App/images/wechat.png create mode 100644 App/main.cpp create mode 100644 CMakeLists.txt create mode 100644 Config.h.in create mode 100644 Designer/CMakeLists.txt create mode 100644 Designer/DesignerGlobal.h create mode 100644 Designer/QiliDesigner.cpp create mode 100644 Designer/QiliDesigner.h create mode 100644 Designer/QiliTextFieldDesigner.cpp create mode 100644 Designer/QiliTextFieldDesigner.h create mode 100644 Designer/QiliTitleBarDesigner.cpp create mode 100644 Designer/QiliTitleBarDesigner.h create mode 100644 FindWrapBrotli.cmake create mode 100644 IFW/config/controller.qs create mode 100644 IFW/config/general.xml create mode 100644 IFW/packages/me.sauntor.qili/meta/desktop.qs create mode 100644 IFW/packages/me.sauntor.qili/meta/package.xml create mode 100644 IFW/packages/me.sauntor.qili/meta/zh_CN.ts create mode 100644 LICENSE create mode 100644 Linux/deb/postinst create mode 100644 Linux/deb/postrm create mode 100644 Linux/me.sauntor.qili.desktop.in create mode 100644 Linux/rpm/postinst create mode 100644 Linux/rpm/postrm create mode 100644 QiliCPack.cmake create mode 100644 QiliIFW.cmake create mode 100644 QiliInstall.cmake create mode 100644 README.md create mode 100644 README_en.md create mode 100644 Thirdparty/CMakeLists.txt create mode 100644 Thirdparty/QiliQRCode.cpp create mode 100644 Thirdparty/QiliQRCode.h create mode 100644 Thirdparty/ThirdpartyExports.h create mode 100644 Thirdparty/qrcodegen.cpp create mode 100644 Thirdparty/qrcodegen.hpp create mode 100644 Widgets/CMakeLists.txt create mode 100644 Widgets/QiliDialog.cpp create mode 100644 Widgets/QiliDialog.h create mode 100644 Widgets/QiliTextField.cpp create mode 100644 Widgets/QiliTextField.h create mode 100644 Widgets/QiliTitleBar.cpp create mode 100644 Widgets/QiliTitleBar.h create mode 100644 Widgets/QiliTitleBar.ui create mode 100644 Widgets/QiliWidgets.qrc create mode 100644 Widgets/QiliWidgets_zh_CN.ts create mode 100644 Widgets/WidgetsGlobal.h create mode 100644 Widgets/images/bili-logo.svg create mode 100644 Widgets/images/bili-user.svg create mode 100644 Widgets/images/clear.svg create mode 100644 Widgets/images/clear_on.svg create mode 100644 Widgets/images/close.svg create mode 100644 Widgets/images/close_on.svg create mode 100644 Widgets/images/help.svg create mode 100644 Widgets/images/qili.ico create mode 100644 Widgets/images/qili.png create mode 100644 Widgets/images/save.svg create mode 100644 Widgets/themes/light.css create mode 100644 changelog diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a0b530 --- /dev/null +++ b/.gitignore @@ -0,0 +1,74 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* +CMakeLists.txt.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt new file mode 100644 index 0000000..3f9c77a --- /dev/null +++ b/App/CMakeLists.txt @@ -0,0 +1,229 @@ +# Copyright (C) 2024 Sauntor + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +################################ +# Main executable for Qili # +################################ + +set(APP_TS_FILES QiliApp_zh_CN.ts) +set(APP_RC_FILES QiliApp.qrc) +set(APP_SRC_FILES + +QiliGlobal.h +QiliAppGlobal.h +Utility.h +Utility.cpp + +QiliCookieJar.h +QiliCookieJar.cpp +QiliHttp.h +QiliHttp.cpp + +QiliProtocol.h +QiliProtocol.cpp +QiliSocket.h +QiliSocket.cpp + +QiliSettings.h +QiliSettings.cpp +QiliSpeaker.h +QiliSpeaker.cpp +QiliConnection.h +QiliConnection.cpp + +QiliSettingsDialog.h +QiliSettingsDialog.cpp +QiliSettingsDialog.ui +QiliLogin.h +QiliLogin.cpp +QiliLogin.ui +QiliSubtitleLogger.h +QiliSubtitleLogger.cpp +QiliSubtitleLogger.ui +QiliThanksDialog.h +QiliThanksDialog.cpp +QiliThanksDialog.ui + +QiliTray.h +QiliTray.cpp + +QiliApp.h +QiliApp.cpp +) + + +qili_add_library(QiliApp) +target_sources(QiliApp PRIVATE + ${APP_SRC_FILES} + ${APP_RC_FILES} + ${APP_TS_FILES} +) +target_compile_definitions(QiliApp PRIVATE QILI_APP_LIBRARY) +add_dependencies(QiliApp QRCodeGen QiliWidgets) + +if (QT_VERSION_MAJOR EQUAL 5) + qt5_create_translation(APP_QM_FILES ${APP_SRC_FILES} ${APP_TS_FILES}) + add_custom_target(QiliApp_lrelease ALL DEPENDS ${APP_QM_FILES}) +else() + qt_add_translations(QiliApp TS_FILES ${APP_TS_FILES} QM_FILES_OUTPUT_VARIABLE APP_QM_FILES) +endif() + +target_link_libraries(QiliApp + PRIVATE + Qt::Core + Qt::Network + Qt::WebSockets + Qt::TextToSpeech + Qt::Widgets + WrapBrotli::WrapBrotliDec + $ + $ +) + +target_include_directories(QiliApp PRIVATE ${CMAKE_BINARY_DIR}) +target_include_directories(QiliApp PRIVATE ${CMAKE_SOURCE_DIR}/Thirdparty) +target_include_directories(QiliApp PRIVATE ${CMAKE_SOURCE_DIR}/Widgets) + +if(${QT_VERSION} VERSION_LESS 6.1.0) + set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER me.sauntor.Qili) +endif() +set_target_properties(QiliApp PROPERTIES + ${BUNDLE_ID_OPTION} + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE +) + +install(TARGETS QiliApp + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT Runtime +) + +install(FILES ../Widgets/images/qili.png DESTINATION ${CMAKE_INSTALL_DATADIR}/pixmaps) +install(FILES ${APP_QM_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/translations) + +qili_finalize_target(QiliApp) + + +qili_add_executable(QiliLauncher) +set_target_properties(QiliLauncher PROPERTIES OUTPUT_NAME Qili) +target_include_directories(QiliLauncher PRIVATE ${CMAKE_BINARY_DIR}) +target_sources(QiliLauncher PRIVATE + main.cpp + QiliLogger.h + QiliLogger.cpp +) +if (WIN32) + target_sources(QiliLauncher PRIVATE Qili.rc) +endif() +add_dependencies(QiliLauncher QiliApp) +target_link_libraries(QiliLauncher PRIVATE Qt::Core Qt::Gui Qt::Widgets) +install(TARGETS QiliLauncher + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT Runtime +) + +if (USE_IFW OR WIN32 OR APPLE) + + if (WIN32 OR APPLE) + set(BROTLI_LIB_COM "${CMAKE_SHARED_LIBRARY_PREFIX}brotlicommon${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(BROTLI_LIB_ENC "${CMAKE_SHARED_LIBRARY_PREFIX}brotlienc${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(BROTLI_LIB_DEC "${CMAKE_SHARED_LIBRARY_PREFIX}brotlidec${CMAKE_SHARED_LIBRARY_SUFFIX}") + if (NOT BROTLI_PATH) + find_path(BROTLI_PATH + NAMES ${BROTLI_LIB_COM} ${BROTLI_LIB_ENC} ${BROTLI_LIB_DEC} + ) + endif() + if (NOT BROTLI_PATH) + if (WIN32) + find_path(BROTLI_PATH + NAMES ${BROTLI_LIB_COM} ${BROTLI_LIB_ENC} ${BROTLI_LIB_DEC} + # trying to find it from vcpkg + HINTS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin" + ) + elseif(APPLE) + message(FATAL_ERROR "adopt Qili to MacOS and delete this line") + endif() + else() + find_path(BROTLI_PATH + NAMES ${BROTLI_LIB_COM} ${BROTLI_LIB_ENC} ${BROTLI_LIB_DEC} + HINTS "${BROTLI_PATH}" "${BROTLI_PATH}/bin" "${BROTLI_PATH}/lib64" "${BROTLI_PATH}/lib" + ) + endif() + + if (NOT BROTLI_PATH) + message(FATAL_ERROR "Please build with -DBROTLI_PATH=/path/to/brolti/libraries/ !") + else() + message(STATUS "Build with Brotli binaries from ${BROTLI_PATH}") + endif() + + install(FILES + ${BROTLI_PATH}/${BROTLI_LIB_COM} + ${BROTLI_PATH}/${BROTLI_LIB_ENC} + ${BROTLI_PATH}/${BROTLI_LIB_DEC} + DESTINATION ${CMAKE_INSTALL_BINDIR} + ) + + qt_generate_deploy_script( + TARGET QiliLauncher + OUTPUT_SCRIPT deploy_script + CONTENT [[ + set(QT_DEPLOY_TRANSLATIONS_DIR "${CMAKE_INSTALL_DATADIR}/translations") + qt_deploy_translations(LOCALES en zh_CN zh_TW) + qt_deploy_runtime_dependencies( + EXECUTABLE $ + ADDITIONAL_LIBRARIES + $ + $ + $ + BIN_DIR ${CMAKE_INSTALL_BINDIR} + LIB_DIR ${CMAKE_INSTALL_LIBDIR} + GENERATE_QT_CONF + NO_TRANSLATIONS + VERBOSE + ) + ]] + ) + endif() + + if (LINUX) + qt_generate_deploy_script( + TARGET QiliLauncher + OUTPUT_SCRIPT deploy_script + CONTENT [[ + qt_deploy_runtime_dependencies( + EXECUTABLE $ + ADDITIONAL_LIBRARIES + $ + $ + $ + BIN_DIR ${CMAKE_INSTALL_BINDIR} + LIB_DIR ${CMAKE_INSTALL_LIBDIR} + NO_TRANSLATIONS + VERBOSE + ) + ]] + ) + endif() + + install(SCRIPT ${deploy_script}) +endif() + +qili_finalize_executable(QiliLauncher) diff --git a/App/Config.h.in b/App/Config.h.in new file mode 100644 index 0000000..2475713 --- /dev/null +++ b/App/Config.h.in @@ -0,0 +1,27 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef CONFIG_H +#define CONFIG_H + +#define QILI_VERSION "@Qili_VERSION@" +#define QILI_RELEASE_BUILD @QILI_RELEASE_BUILD@ +#cmakedefine01 QILI_ON_WIN32 +#cmakedefine01 QILI_ON_LINUX +#cmakedefine01 QILI_ON_UNIX +#cmakedefine01 QILI_ON_APPLE + +#endif // CONFIG_H diff --git a/App/QiliApp.cpp b/App/QiliApp.cpp new file mode 100644 index 0000000..812a0e1 --- /dev/null +++ b/App/QiliApp.cpp @@ -0,0 +1,107 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliApp.h" + +#include "QiliGlobal.h" +#include "QiliTray.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Qili; + +int QiliApp(QApplication &app) +{ + app.setApplicationVersion(ApplicationVersion); + app.setApplicationName(ApplicationName); + app.setOrganizationName(OrganizationName); + app.setOrganizationDomain(OrganizationDomain); + + QDir binDir = QCoreApplication::applicationDirPath(); + QDir appDir = binDir.absoluteFilePath(".."); + + QTranslator qtranslator; + if (qtranslator.load(QLocale::system(), QString("qt"), QString("_"), +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QLibraryInfo::path(QLibraryInfo::TranslationsPath)) +#else + QLibraryInfo::location(QLibraryInfo::TranslationsPath)) +#endif + ) { + app.installTranslator(&qtranslator); + } else { + qWarning() << "Can't load Qt Library Translations"; + } + + const QStringList uiLanguages = QLocale::system().uiLanguages(); + const QStringList components = {"App", "Widgets"}; + for (const auto &component : std::as_const(components)) { + QTranslator *translator = new QTranslator(&app); + auto found = false; + for (const QString &locale : uiLanguages) { + const QString baseName = "Qili" + component + "_" + QLocale(locale).name(); + if (translator->load(":/i18n/" + baseName) || + // For Debug Environment + translator->load(appDir.filePath(component + "/") + baseName) || + // For Release Environment + translator->load(appDir.filePath("share/translations/") + baseName)) { + qDebug() << "translation loaded: " << translator->filePath(); + found = true; + break; + } + } + if (found) { + app.installTranslator(translator); + } else { + delete translator; + } + } + + + QFile theme(":/themes/light.css"); + if (theme.open(QFile::ReadOnly)) { + auto styles = theme.readAll(); + app.setStyleSheet(styles); + } + app.setQuitOnLastWindowClosed(false); + + + QiliTray tray; + tray.show(); + + + return app.exec(); +} diff --git a/App/QiliApp.h b/App/QiliApp.h new file mode 100644 index 0000000..01d0067 --- /dev/null +++ b/App/QiliApp.h @@ -0,0 +1,28 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILIAPP_H +#define QILIAPP_H + +#include "QiliAppGlobal.h" + +#include + +extern "C" { + QILI_APP_EXPORT int QiliApp(QApplication &app); +} + +#endif // QILIAPP_H diff --git a/App/QiliApp.qrc b/App/QiliApp.qrc new file mode 100644 index 0000000..187a849 --- /dev/null +++ b/App/QiliApp.qrc @@ -0,0 +1,6 @@ + + + images/alipay.png + images/wechat.png + + diff --git a/App/QiliAppGlobal.h b/App/QiliAppGlobal.h new file mode 100644 index 0000000..05622d3 --- /dev/null +++ b/App/QiliAppGlobal.h @@ -0,0 +1,28 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILIAPPGLOBAL_H +#define QILIAPPGLOBAL_H + +#include + +#if defined(QILI_APP_LIBRARY) +#define QILI_APP_EXPORT Q_DECL_EXPORT +#else +#define QILI_APP_EXPORT Q_DECL_IMPORT +#endif + +#endif // QILIAPPGLOBAL_H diff --git a/App/QiliApp_zh_CN.ts b/App/QiliApp_zh_CN.ts new file mode 100644 index 0000000..799180d --- /dev/null +++ b/App/QiliApp_zh_CN.ts @@ -0,0 +1,448 @@ + + + + + QiliLauncher + + + + Qili + Qili + + + + + You can find it on the broadcasting page + 你可以在开播页面找到 + + + + + + + If you start as anonymous, visitor's name in the room will not be shadowed + 若匿名登录,你将无法看到完整的房间访客姓名 + + + + + You can start withou scanning, just as anonymous + 你可以不登录,而用匿名开始 + + + + + As Anonymous + 匿名登录 + + + + + Remember Me + 记住我 + + + + + Start + 开始 + + + + + Click to refresh + 点击刷新 + + + + + Please enter the number + 请输入房间号 + + + + + ROOM + 房间号 + + + + + Scan QrCode + 登录扫码 + + + + QrCode goes invalid + 二维码已失效 + + + + QiliSettingsDialog + + + + + + Qili Settings + Qili配置 + + + + + Basic + 基础 + + + + + Remember Room + 记住房间 + + + + + Remember User + 记住用户 + + + + + Voices + 语音 + + + + + Language + 语言 + + + + + Voice + 音色 + + + + + Volume + 音量 + + + + + Pitch + 语速 + + + + + This is the text to speech + 这里是测试发音文字 + + + + + Test + 测试 + + + + + Apply + 应用 + + + + + + Qili + Qili + + + + Do you want login bilibili.com now? + 你想立即登录B站吗? + + + + Are you sure to clear the authentication for bilibili.com? + 你确定要清除B站的认证数据吗? + + + + Are you sure to clear the stored room number? + 你确定清除已保存的房间号吗? + + + + QiliSubtitleLogger + + + + + + Subtitle Logs + 弹幕记录 + + + + QiliThanksDialog + + + + + + About Qili + 关于 Qili + + + + + An open source and free subtitle spearker for live broadcasting at bilibili.com + A subtitle spearker for live broadcasting at bilibili.com + 一个开源且免费的B站直播弹幕播报工具 + + + + + Thanks + Thanks to: + 鸣谢 + + + + + Sauntor <sauntor@live.com> (Author) + Sauntor <sauntor@live.com> + 适然 <sauntor@live.com> (作者) + + + + + Sponsor + 赞助 + + + + + Alipay + 支付宝 + + + + + Wechat + 微信 + + + + QiliTray + + &Connect + 连接(&C) + + + + Re&Connect + 重连(&C) + + + + &Logger + 弹幕(&L) + + + + &Settings + 设置(&S) + + + + &Thanks + 鸣谢(&T) + + + + &Restart + 重启(&R) + + + + &Exit + 退出(&E) + + + + + Qili + Qili + + + + Connected now, to exit use the system tray. + 弹幕连接成功,已隐藏到系统托盘。 + + + + %1 says %2 + %1 说 %2 + + + + %1 enter room + 欢迎 %1 进入直播间 + + + + %1 %2 %3 %4 x %5 + %1 %2 %3 %4 乘以 %5 + + + + %1 visitors until now + 本场直播累计访客%1人 + + + + %1 Watcher(s) + 当前人气 %1 + + + + [%1] Watcher(s): %2 + [%1] 人气: %2 + + + + + + Qili Speaker + Qili语音 + + + + Resumed + 播报已暂停 + + + + Paused + 已继续播报 + + + + Qili disconnected + 弹幕连接已断开 + + + + Settings applied + 已应用语音设置 + + + + Text + + 0 + + + + 1 + + + + 2 + + + + 3 + + + + 4 + + + + 5 + + + + 6 + + + + 7 + + + + 8 + + + + 9 + + + + + 0 + speakable-username + + + + + 1 + speakable-username + + + + + 2 + speakable-username + + + + + 3 + speakable-username + + + + + 4 + speakable-username + + + + + 5 + speakable-username + + + + + 6 + speakable-username + + + + + 7 + speakable-username + + + + + 8 + speakable-username + + + + + 9 + speakable-username + + + + diff --git a/App/QiliConnection.cpp b/App/QiliConnection.cpp new file mode 100644 index 0000000..4939305 --- /dev/null +++ b/App/QiliConnection.cpp @@ -0,0 +1,83 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#include "QiliConnection.h" + +#include "QiliGlobal.h" +#include "Utility.h" + +#include + +using namespace Utility::JSON; + +QiliConnection::QiliConnection(QiliHttp *http, QObject *parent) + : QObject(parent), + mHttp(http) +{ + mSocket = new QiliSocket(this); + + QObject::connect(mSocket, &QiliSocket::authenticated, this, &QiliConnection::authenticated); + QObject::connect(mSocket, &QiliSocket::watchersChanged, this, &QiliConnection::onWatchersCount); + QObject::connect(mSocket, &QiliSocket::subtitleReceived, this, &QiliConnection::subtitleReceived); + + QObject::connect(mSocket, &QiliSocket::errorOccured, this, &QiliConnection::onSocketError); +} + +void QiliConnection::setRoom(int room) +{ + mRoom = room; +} + +void QiliConnection::connect() +{ + HttpReply reply = mHttp->get(Qili::Uris::GetDanmuInfo, {{"id", mRoom}}); + QJsonObject json = reply->readAll() >> JObject; + QJsonObject data = json / "data" >> JObject; + QString token = data / "token" >> JString; + QJsonArray array = data.value("host_list").toArray(); + auto item = array.first() >> JObject; + auto wslink = QString("wss://%1:%2/sub").arg(item / "host" >> JString).arg(item / "wss_port" >> JInt); + QJsonObject auth { + {"uid", mHttp->buid()}, + {"roomid", mRoom}, + {"protover", 3}, + {"platform", "web"}, + {"type", 2}, + {"key", token} + }; + qDebug() << "WebSocket Auth Body: " << auth; + mSocket->setAuth(auth >> JString); + mSocket->setUrl(wslink); + mSocket->open(); +} + +void QiliConnection::disconnect() +{ + mSocket->close(); +} + +void QiliConnection::onWatchersCount(int watchers) +{ + if (watchers != mWatchers) { + mWatchers = watchers; + emit watchersChanged(watchers); + } +} + +void QiliConnection::onSocketError(const QString &errorString) +{ + emit errorOccured(SocketError, errorString); +} + diff --git a/App/QiliConnection.h b/App/QiliConnection.h new file mode 100644 index 0000000..1efae19 --- /dev/null +++ b/App/QiliConnection.h @@ -0,0 +1,60 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#ifndef QILICONNECTION_H +#define QILICONNECTION_H + +#include "QiliAppGlobal.h" +#include "QiliSocket.h" +#include "QiliHttp.h" + +#include +#include + +class QILI_APP_EXPORT QiliConnection : public QObject +{ + Q_OBJECT + +public: + enum Error { StartFailed, EndFailed, SocketError }; + + explicit QiliConnection(QiliHttp *http, QObject *parent = nullptr); + +signals: + void authenticated(); + void watchersChanged(int watchers); + void subtitleReceived(const QJsonObject &subtitle); + void errorOccured(Error error, const QString &message); + +public slots: + void setRoom(int room); + void connect(); + void disconnect(); + +private slots: + void onWatchersCount(int watchers); + // void onSubtitleReceived(const QJsonObject &subtitle); + // void onAuthenticated(); + void onSocketError(const QString &errorString); + +private: + int mRoom{0}; + int mWatchers{0}; + QiliSocket *mSocket; + QiliHttp *mHttp; + +}; + +#endif // QILICONNECTION_H diff --git a/App/QiliCookieJar.cpp b/App/QiliCookieJar.cpp new file mode 100644 index 0000000..54acf79 --- /dev/null +++ b/App/QiliCookieJar.cpp @@ -0,0 +1,59 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliCookieJar.h" + +#include +#include + +QiliCookieJar::QiliCookieJar(QObject *parent) + : QNetworkCookieJar(parent) +{} + +QiliCookieJar::~QiliCookieJar() +{} + +QList QiliCookieJar::allCookies() const +{ + return QNetworkCookieJar::allCookies(); +} + +bool QiliCookieJar::insertCookie(const QNetworkCookie &cookie) +{ + bool inserted = QNetworkCookieJar::insertCookie(cookie); + if (inserted) { + emit cookieInserted(cookie); + } + return inserted; +} + +bool QiliCookieJar::updateCookie(const QNetworkCookie &cookie) +{ + bool updated = QNetworkCookieJar::updateCookie(cookie); + if (updated) { + emit cookieUpdated(cookie); + } + return updated; +} + +bool QiliCookieJar::deleteCookie(const QNetworkCookie &cookie) +{ + bool deleted = QNetworkCookieJar::deleteCookie(cookie); + if (deleted) { + emit cookieDeleted(cookie); + } + return deleted; +} diff --git a/App/QiliCookieJar.h b/App/QiliCookieJar.h new file mode 100644 index 0000000..3ce007a --- /dev/null +++ b/App/QiliCookieJar.h @@ -0,0 +1,45 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILICOOKIEJAR_H +#define QILICOOKIEJAR_H + +#include "QiliAppGlobal.h" + +#include +#include + +class QILI_APP_EXPORT QiliCookieJar : public QNetworkCookieJar +{ + Q_OBJECT + +public: + explicit QiliCookieJar(QObject *parent = nullptr); + ~QiliCookieJar(); + + QList allCookies() const; + + virtual bool insertCookie(const QNetworkCookie &cookie) override; + virtual bool updateCookie(const QNetworkCookie &cookie) override; + virtual bool deleteCookie(const QNetworkCookie &cookie) override; + +signals: + void cookieInserted(QNetworkCookie cookie); + void cookieUpdated(QNetworkCookie cookie); + void cookieDeleted(QNetworkCookie cookie); +}; + +#endif // QILICOOKIEJAR_H diff --git a/App/QiliGlobal.h b/App/QiliGlobal.h new file mode 100644 index 0000000..631b02f --- /dev/null +++ b/App/QiliGlobal.h @@ -0,0 +1,84 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILIGLOBAL_H +#define QILIGLOBAL_H + +#include "Config.h" + +#include + +#define QILI_BEGIN namespace Qili { +#define QILI_END }; + +#define QILI_NS_BEGIN(NS) namespace Qili::NS { +#define QILI_NS_END(NS) }; + +namespace Qili { + +const bool Released = QILI_RELEASE_BUILD; + + enum QiliFlag { Restart = 'R' + 'S' + 'T' }; + + constexpr const char * const ApplicationName = "Qili"; + constexpr const char * const ApplicationVersion = QILI_VERSION; + constexpr const char * const ApplicationDisplayName = "DisplayName"; + constexpr const char * const OrganizationName = "Sauntor OSS"; + constexpr const char * const OrganizationDomain = "sauntor.me"; + constexpr const char * const UserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; + + + + namespace Uris { + constexpr const char * const PlayLive = "https://play-live.bilibili.com"; +#define BILI_LIEV_BASE_URL "https://live-open.biliapi.com" + constexpr const char * const LiveApi = BILI_LIEV_BASE_URL; + constexpr const char * const LiveStart = BILI_LIEV_BASE_URL "/v2/app/start"; + constexpr const char * const LiveEnd = BILI_LIEV_BASE_URL "/v2/app/end"; +#undef BILI_LIEV_BASE_URL + constexpr const char * const GenerateQrcode = "https://passport.bilibili.com/x/passport-login/web/qrcode/generate"; + constexpr const char * const PollQrcode = "https://passport.bilibili.com/x/passport-login/web/qrcode/poll"; + constexpr const char * const GetDanmuInfo = "https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo"; + }; + + namespace SettingKeys { + constexpr const char * const Cookies = "cookies"; + constexpr const char * const KeepUser = "keep_user"; + constexpr const char * const KeepRoom = "keep_room"; + constexpr const char * const Room = "room"; + constexpr const char * const Lang = "lang"; + constexpr const char * const Voice = "voice"; + constexpr const char * const Volume = "volume"; + constexpr const char * const Pitch = "pitch"; + }; + + namespace Widgets { + constexpr const char * const PROPERTY = "qili-widget"; + constexpr const char * const TitleBar = "titlebar"; + constexpr const char * const TextField = "text-field"; + }; + namespace Buttons { + constexpr const char * const PROPERTY = "qili-btn"; + constexpr const char * const Radio = "radio"; + constexpr const char * const Primary = "primary"; + } +}; + +// Keep these macros from Config.h only lives in this header +#undef QILI_VERSION +#undef QILI_RELEASE_BUILD + +#endif // QILIGLOBAL_H diff --git a/App/QiliHttp.cpp b/App/QiliHttp.cpp new file mode 100644 index 0000000..e97c5e2 --- /dev/null +++ b/App/QiliHttp.cpp @@ -0,0 +1,184 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliHttp.h" + +#include "QiliGlobal.h" + +#include +#include +#include +#include +#include + + +QiliHttp::QiliHttp(QObject *parent) +{ + networkAccessManager = new QNetworkAccessManager(); + mCookieJar = new QiliCookieJar(); + networkAccessManager->setCookieJar(mCookieJar); +} + +QiliHttp::~QiliHttp() +{ + delete networkAccessManager; +} + +QNetworkRequest QiliHttp::newRequest(const QString &url, Parameters parameters, int timeout) +{ + QUrl urlWithQueryString(url); + QUrlQuery query(url); + for (const auto ¶meter : parameters) { + query.addQueryItem(parameter.first, parameter.second.toString()); + } + urlWithQueryString.setQuery(query); + QNetworkRequest request(urlWithQueryString); + request.setHeader(QNetworkRequest::UserAgentHeader, Qili::UserAgent); + if (timeout == 0) { + // Use the default timeout value: 30s + request.setTransferTimeout(QNetworkRequest::DefaultTransferTimeoutConstant); + } + if (timeout > 0) { + request.setTransferTimeout(timeout); + } + return request; +} + +QiliCookieJar *QiliHttp::cookieJar() const +{ + return mCookieJar; +} + +QDataStream &operator<<(QDataStream &stream, const QNetworkCookie &cookie) +{ + stream << cookie.toRawForm(); + return stream; +} + +QDataStream &operator>>(QDataStream &stream, QNetworkCookie &cookie) +{ + QByteArray bytes; + stream >> bytes; + auto cookies = QNetworkCookie::parseCookies(bytes); + cookie = cookies.first(); + return stream; +} + +QByteArray QiliHttp::storabelCookies() const +{ + QByteArray bytes; + QDataStream io(&bytes, QIODevice::WriteOnly); + auto cookies = mCookieJar->allCookies(); + io << cookies; + return bytes; +} + +void QiliHttp::restoreCookies(const QByteArray &data) +{ + if (data.isEmpty()) { + auto cookies = mCookieJar->allCookies(); + for (const auto &cookie : std::as_const(cookies)) { + mCookieJar->deleteCookie(cookie); + } + return; + } + QDataStream io((QByteArray *) &data, QIODevice::ReadOnly); + + QList cookies; + io >> cookies; + for (const auto &cookie : std::as_const(cookies)) { + mCookieJar->deleteCookie(cookie); + mCookieJar->insertCookie(cookie); + } +} + +int QiliHttp::buid() +{ + auto cookies = mCookieJar->allCookies(); + for (const auto &cookie : std::as_const(cookies)) { + if (cookie.name() == "DedeUserID") { + return cookie.value().toInt(); + } + } + return 0; +} + +QDateTime QiliHttp::expires() +{ + auto cookies = mCookieJar->allCookies(); + auto found = std::find_if(cookies.constBegin(), cookies.constEnd(), [](QNetworkCookie v){ + return v.name() == "SESSDATA"; + }); + if (found != cookies.constEnd()) { + return found->expirationDate(); + } + return QDateTime::fromMSecsSinceEpoch(-1); +} + +// QSharedPointer QiliHttp::bapi(const QString &url, const QByteArray &data, int timeout) +// { +// QString md5 = QCryptographicHash::hash(data, QCryptographicHash::Md5).toHex().toLower(); +// QString accessKeyId= Qili::ACC_KEY_ID; +// QString accessKeySecret = Qili::ACC_KEY_SECRET; +// QString nonce = QUuid::createUuid().toString(); +// qint64 timestamp = QDateTime::currentSecsSinceEpoch(); +// QString sign; +// sign += "x-bili-accesskeyid:" + accessKeyId + "\n"; +// sign += "x-bili-content-md5:" + md5 + "\n"; +// sign += "x-bili-signature-method:HMAC-SHA256\n"; +// sign += "x-bili-signature-nonce:" + nonce; +// sign += "\nx-bili-signature-version:1.0\n"; +// sign += QString("x-bili-timestamp:%1").arg(timestamp); +// QString signature = QMessageAuthenticationCode::hash( +// sign.toUtf8(), accessKeySecret.toUtf8(), QCryptographicHash::Sha256).toHex().toLower(); + +// QNetworkRequest request = newRequest(url, {}, timeout); + +// request.setRawHeader("x-bili-accesskeyid", accessKeyId.toLocal8Bit()); +// request.setRawHeader("x-bili-content-md5", md5.toLocal8Bit()); +// request.setRawHeader("x-bili-signature-method", "HMAC-SHA256"); +// request.setRawHeader("x-bili-signature-nonce", nonce.toLocal8Bit()); +// request.setRawHeader("x-bili-signature-version", "1.0"); +// request.setRawHeader("x-bili-timestamp", QString("%1").arg(timestamp).toLocal8Bit()); +// request.setRawHeader("Authorization", signature.toLocal8Bit()); +// request.setRawHeader("Accept", "application/json"); +// request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + +// QNetworkReply *reply = networkAccessManager->post(request, data); +// QEventLoop replyWaiter; +// QObject::connect(reply, &QNetworkReply::finished, &replyWaiter, &QEventLoop::quit); +// QObject::connect(reply, &QNetworkReply::errorOccurred, &replyWaiter, &QEventLoop::quit); +// replyWaiter.exec(QEventLoop::ExcludeUserInputEvents); + +// return HttpReply(reply, &QNetworkReply::deleteLater); +// } + +QSharedPointer QiliHttp::get(const QString &url, Parameters parameters, int timeout) +{ + QNetworkReply *reply = getAsync(url, parameters, timeout); + QEventLoop replyWaiter; + QObject::connect(reply, &QNetworkReply::finished, &replyWaiter, &QEventLoop::quit); + QObject::connect(reply, &QNetworkReply::errorOccurred, &replyWaiter, &QEventLoop::quit); + replyWaiter.exec(QEventLoop::ExcludeUserInputEvents); + + return HttpReply(reply, &QNetworkReply::deleteLater); +} + +QNetworkReply *QiliHttp::getAsync(const QString &url, Parameters parameters, int timeout) +{ + QNetworkRequest request = newRequest(url, parameters, timeout); + return networkAccessManager->get(request); +} diff --git a/App/QiliHttp.h b/App/QiliHttp.h new file mode 100644 index 0000000..ff6129f --- /dev/null +++ b/App/QiliHttp.h @@ -0,0 +1,72 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILIHTTP_H +#define QILIHTTP_H + +#include "QiliAppGlobal.h" +#include "QiliCookieJar.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +using HttpReply = QSharedPointer; + +class QILI_APP_EXPORT QiliHttp : QObject +{ + Q_OBJECT + + using Parameters = std::initializer_list>; + +public: + explicit QiliHttp(QObject *parent = nullptr); + ~QiliHttp(); + + static QNetworkRequest newRequest(const QString &url, Parameters parameters = {}, int timeout = -1); + + QiliCookieJar *cookieJar() const; + QByteArray storabelCookies() const; + + void restoreCookies(const QByteArray &data = QByteArray()); + + int buid(); + QDateTime expires(); + HttpReply bapi(const QString &url, const QByteArray &data, int timeout = -1); + + HttpReply get(const QString &url, Parameters parameters = {}, int timeout = -1); + + QNetworkReply *getAsync(const QString &url, Parameters parameters = {}, int timeout = -1); + +private: + QNetworkAccessManager *networkAccessManager; + QiliCookieJar *mCookieJar; +}; + +#endif // QILIHTTP_H diff --git a/App/QiliLauncher2.ui b/App/QiliLauncher2.ui new file mode 100644 index 0000000..cc33adb --- /dev/null +++ b/App/QiliLauncher2.ui @@ -0,0 +1,347 @@ + + + + BilibiliGetCodeDialog + + + + 0 + 0 + 400 + 250 + + + + + + 0 + 0 + 400 + 250 + + + + BilibiliGetCodeDialog + + + QWidget#widget_1{ + border-width: 1px; + border-style: solid; + border-color: rgb(211, 215, 222); + border-radius:5px; + background:white; +} +QPushButton{ + border: none; +} + + + + + 0 + 150 + 138 + 100 + + + + + + + :/image/bili-logo.svg + + + + + + 110 + 16 + 180 + 29 + + + + + -1 + false + + + + QLabel#label_2{ + font-size: 18px; + font-weight: normal; + line-height: 29px; + letter-spacing: 0px; + color: #FF6699; +} + + + 认证身份后可开启玩法 + + + + + + 141 + 175 + 119 + 32 + + + + QPushButton#startButton{ + font-size: 14px; + font-weight: normal; + line-height: 20px; + letter-spacing: 0px; + border-radius: 4px; + background: #FF6699; + color: #FFFFFF; +} +QPushButton#startButton:hover{ + background-color:rgb(255, 79, 135); +} +QPushButton#startButton:pressed{ + background-color:rgb(220, 88, 132); +} + + + 开启玩法 + + + + + + 155 + 215 + 95 + 20 + + + + QRadioButton#radioButton{ + font-size: 12px; + font-weight: normal; + line-height: 17px; + letter-spacing: 0px; + color: #61666D; +} +QRadioButton#radioButton::indicator { + width: 15px; + height: 15px; +} +QRadioButton#radioButton::indicator:checked { + image: url(:/image/save.svg); +} + + + 记住身份码 + + + + + + 372 + 15 + 13 + 13 + + + + QPushButton#closeButton{ + image:url(":/image/close.svg"); +} +QPushButton#closeButton:hover{ + image:url(":/image/close_on.svg"); +} +QPushButton#closeButton:pressed{ +} + + + + + + + + + 46 + 114 + 15 + 15 + + + + 在获取推流地址处可获取身份码 + + + + + + :/image/help.svg + + + + + + 66 + 112 + 168 + 17 + + + + 在获取推流地址处可获取身份码 + + + QLabel#label_4{ + font-size: 12px; + font-weight: normal; + line-height: 17px; + letter-spacing: 0px; + color: #C9CCD0; +} + + + 在获取推流地址处可获取身份码 + + + + + + 319 + 110 + 36 + 17 + + + + QPushButton#getCodeButton{ + font-size: 12px; + font-weight: normal; + line-height: 17px; + letter-spacing: 0px; + color: #FF6699; +} +QPushButton#getCodeButton:hover{ + text-decoration:underline +} +QPushButton#getCodeButton:pressed{ + color:rgb(220, 88, 132); +} + + + 去获取 + + + + + + 45 + 75 + 310 + 33 + + + + QWidget#widget{ + border: 1px solid #E3E5E7; + border-radius: 4px; +} + + + + + 0 + 0 + 66 + 33 + + + + QLabel#label_5{ + background: #F6F7F8; + border: 1px solid #E3E5E7; + font-size: 14px; + font-weight: normal; + line-height: 20px; + letter-spacing: 0px; + color: #18191C; +} + + + 身份码 + + + Qt::AlignCenter + + + + + + 70 + 1 + 210 + 31 + + + + + -1 + false + + + + QLineEdit#codeLineEdit{ + border:one; + font-size: 14px; + font-weight: normal; + line-height: 20px; + letter-spacing: 0px; +} + + + QLineEdit::Password + + + 请输入身份码 + + + + + + 292 + 12 + 10 + 10 + + + + QPushButton#clearButton{ + image:url(":/image/clear.svg"); +} +QPushButton#clearButton:hover{ + image:url(":/image/clear_on.svg"); +} + + + + + + + + + + + + + diff --git a/App/QiliLogger.cpp b/App/QiliLogger.cpp new file mode 100644 index 0000000..c82469b --- /dev/null +++ b/App/QiliLogger.cpp @@ -0,0 +1,117 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliLogger.h" + +#include +#include + +QiliLogger::QiliLogger() : QObject(nullptr) +{ + QDir dir = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); + if (dir.mkpath("logs")) { + mDir = new QDir(dir.absoluteFilePath("logs")); + mCanWrite = true; + } else { + mDir = nullptr; + mCanWrite = false; + } + qSetMessagePattern("[%{type}]%{time}[%{qthreadptr}](%{file}:%{line}): %{message}"); +} + +QiliLogger::~QiliLogger() +{ + if (mFile != nullptr) { + mFile->close(); + delete mFile; + } +} + +void QiliLogger::console(QtMsgType type, const QMessageLogContext &context, const QString &message) +{ + if (mConsole != nullptr) { + mConsole(type, context, message); + } +} + +void QiliLogger::logging(QtMsgType type, const QMessageLogContext &context, const QString &message) +{ + if (!mCanWrite) { + console(type, context, message); + return; + } + auto file = log(); + if (file == nullptr) { + return; + } + console(type, context, message); + auto formated = qFormatLogMessage(type, context, message); + file->write(qUtf8Printable(formated)); + file->write("\n"); + file->flush(); + // std::cout << "LOG: " << formated << std::endl; +} + +void QiliLogging(QtMsgType type, const QMessageLogContext &context, const QString &message) +{ + QiliLogger& logger = QiliLogger::install(); + logger.logging(type, context, message); +} + + +QiliLogger &QiliLogger::install() +{ + static QiliLogger logger; + if (!logger.mInstalled) { + logger.mConsole = qInstallMessageHandler(QiliLogging); + logger.mInstalled = true; + qInfo() << "QiliLogger initialized: has console? " << (logger.mConsole != nullptr); + } + return logger; +} + +QFile *QiliLogger::log() +{ + if (!mCanWrite) { + return nullptr; + } + if (mDir == nullptr) { + return nullptr; + } + QDate today = QDate::currentDate(); + if (mFile == nullptr || mDate != today) { + if (mFile != nullptr) { + mFile->close(); + delete mFile; + } + auto filename = today.toString(Qt::ISODate) + ".log"; + auto file = new QFile(mDir->absoluteFilePath(filename)); + if (!file->open(QFile::ReadWrite)) { + if (mConsole != nullptr) { + mConsole(QtMsgType::QtCriticalMsg, QMessageLogContext(), + QString("Can't create or open log: %1s").arg(file->fileName())); + } + delete file; + mFile = nullptr; + mCanWrite = false; + } + else { + mFile = file; + mCanWrite = true; + } + } + return mFile; +} diff --git a/App/QiliLogger.h b/App/QiliLogger.h new file mode 100644 index 0000000..720ce12 --- /dev/null +++ b/App/QiliLogger.h @@ -0,0 +1,56 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILILOGGER_H +#define QILILOGGER_H + +#include +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#include +#else +#include +#endif +#include +#include +#include +#include + +class QiliLogger : public QObject +{ + Q_OBJECT + +public: + void logging(QtMsgType type, const QMessageLogContext &context, const QString &message); + + static QiliLogger& install(); + + +private: + QiliLogger(); + ~QiliLogger(); + + void console(QtMsgType type, const QMessageLogContext &context, const QString &message); + QFile *log(); + + bool mInstalled{false}; + bool mCanWrite{true}; + QtMessageHandler mConsole{nullptr}; + QFile *mFile{nullptr}; + QDir *mDir{nullptr}; + QDate mDate{QDate::currentDate()}; +}; + +#endif // QILILOGGER_H diff --git a/App/QiliLogin.cpp b/App/QiliLogin.cpp new file mode 100644 index 0000000..fecdd6f --- /dev/null +++ b/App/QiliLogin.cpp @@ -0,0 +1,229 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + + +#include "QiliLogin.h" +#include "ui_QiliLogin.h" + +#include "QiliGlobal.h" +#include "Utility.h" + +#include "QiliQRCode.h" + +#include +#include +#include +#include +#include +#include + +using namespace Qili; +using namespace Utility::JSON; + +QiliLauncher::QiliLauncher(QiliHttp *http, QWidget *parent) : + QiliDialog(parent), + ui(new Ui::QiliLauncher), mHttp(http) +{ + setWindowFlags(Qt::FramelessWindowHint); + ui->setupUi(this); + ui->rememberButton->setChecked(mRemember); + connect(ui->startButton, &QPushButton::clicked, this, &QiliLauncher::onStartClicked); + connect(ui->rememberButton, &QRadioButton::toggled, this, &QiliLauncher::onRememberToggled); + connect(ui->qrcode, &QPushButton::clicked, this, &QiliLauncher::refreshQrCode); + connect(ui->anonymous, &QRadioButton::toggled, this, &QiliLauncher::onAnonymousToggled); +} + +QiliLauncher::~QiliLauncher() +{ + delete ui; +} + + +// overrides +void QiliLauncher::timerEvent(QTimerEvent *event) +{ + if (event->timerId() != mQrTimer) { + QDialog::timerEvent(event); + return; + } + + if (mQrTTL > 0) { + pullQrcode(); + } + else { + clearPulling(); + QMessageBox::warning(this, tr("Scan QrCode"), tr("QrCode goes invalid")); + } + mQrTTL--; +} + +void QiliLauncher::show() +{ + emit ui->anonymous->toggled(mAuthenticated); + if (!mAuthenticated) { + refreshQrCode(); + } + QiliDialog::show(); +} + +// properites +void QiliLauncher::setRemember(bool remember) +{ + ui->rememberButton->setCheckable(remember); +} + +void QiliLauncher::setRoom(int room) +{ + if (room == 0) { + ui->roomField->clear(); + } + else { + ui->roomField->setText(QString("%1").arg(room)); + } +} + +void QiliLauncher::setAuthenticated(bool authenticated) +{ + mAuthenticated = authenticated; + emit ui->anonymous->toggled(authenticated); +} + + +// ui interactions +void QiliLauncher::showError(const QString &message) +{ + QMessageBox::warning(this, tr("Scan QrCode"), message); +} + +void QiliLauncher::onStartClicked() +{ + auto room = ui->roomField->text().toInt(); + auto role = UserRole::Anonymous; + if (!ui->anonymous->isChecked()) { + role = UserRole::Authenticated; + } + emit starting(role, room); +} + +void QiliLauncher::onRememberToggled(bool checked) +{ + mRemember = checked; +} + +void QiliLauncher::onAnonymousToggled(bool checked) +{ + ui->rememberWrapper->setVisible(mAuthenticated || !checked); + ui->qrcode->setVisible(!checked); + ui->roomField->setVisible(checked); + ui->startWidget->setVisible(checked); + emit ui->rememberButton->toggled(!checked); + if (checked) { + clearPulling(); + } + else { + refreshQrCode(); + } +} + + +// methods +void QiliLauncher::refreshQrCode() +{ + clearPulling(); + auto reply = mHttp->get(Qili::Uris::GenerateQrcode); + if (reply->error() != QNetworkReply::NoError) { + showError(reply->errorString()); + return; + } + auto json = reply->readAll() >> JObject; + int code = json >> JInt; + if (code != 0) { + showError(json >> JString); + return; + } + + QString url = json / "data" / "url" >> JString; + mQrKey = json / "data" / "qrcode_key" >> JString; + + QiliQRCode qr(url); + int size = qr.size(); + QImage image(size, size, QImage::Format_RGB32); + for (int x = 0; x < size; x++) { + for (int y = 0; y < size; y++) { + if (qr.module(y, x)) { + image.setPixel(x, y, qRgb(255, 255, 255)); + } else { + image.setPixel(x, y, qRgb(0, 0, 0)); + } + } + } + + QImage scaled = image.scaled(ui->qrcode->width(), ui->qrcode->height(), Qt::KeepAspectRatio); + ui->qrcode->setIcon(QPixmap::fromImage(scaled)); + + qDebug() << "QRCode Generated: " << json; + if (mQrTimer != 0) { + killTimer(mQrTimer); + } + mQrTimer = startTimer(10 * 1000); + mQrTTL = 10 * 60 / 10; + emit refreshed(); +} + +void QiliLauncher::pullQrcode() +{ + qDebug() << "QRCode Pulling..."; + auto reply = mHttp->get(Qili::Uris::PollQrcode, {{"qrcode_key", mQrKey}}); + if (reply->error()) { + showError(reply->errorString()); + return; + } + auto json = reply->readAll() >> JObject; + qDebug() << "QRCode Pulled Success: " << json; + + // qDebug() << "QRCode Login Cookie: " ; + // auto cookieJar = mHttp->cookieJar(); + // auto cookies = cookieJar->allCookies(); + // for (const auto &cookie : std::as_const(cookies)) { + // qDebug() << cookie; + // } + + int code = json / "data" / "code" >> JInt; + if (code == 86038) { + clearPulling(); + showError(json / "data" / "message" >> JString); + } + else if (code == 0) { + qDebug() << "QRCode Login Success"; + clearPulling(); + + mAuthenticated = true; + ui->qrcode->setVisible(false); + ui->roomField->setVisible(true); + ui->anonymousWrapper->setVisible(false); + ui->startWidget->setVisible(true); + } +} + + +void QiliLauncher::clearPulling() +{ + qDebug() << "QRCode Clear Pulling Timer: " << mQrTimer; + if (mQrTimer != 0) { + killTimer(mQrTimer); + mQrTimer = 0; + } +} diff --git a/App/QiliLogin.h b/App/QiliLogin.h new file mode 100644 index 0000000..132e4d1 --- /dev/null +++ b/App/QiliLogin.h @@ -0,0 +1,82 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILILOGIN_H +#define QILILOGIN_H + +#include "QiliAppGlobal.h" +#include "QiliHttp.h" +#include "QiliDialog.h" + +#include + +namespace Ui { +class QiliLauncher; +} + +class QDrag; + +enum UserRole { Anonymous, Authenticated }; + +class QILI_APP_EXPORT QiliLauncher : public QiliDialog +{ + Q_OBJECT + +public: + explicit QiliLauncher(QiliHttp *http, QWidget *parent = nullptr); + ~QiliLauncher(); + + void show(); + +signals: + void refreshed(); + void rememberChanged(bool remember); + void starting(UserRole role, int room); + +public slots: + void setRemember(bool remember); + void setRoom(int room); + void setAuthenticated(bool authenticated); + + +protected: + void timerEvent(QTimerEvent *event) override; + +private slots: + void onRememberToggled(bool checked); + void onAnonymousToggled(bool checked); + void onStartClicked(); + + void refreshQrCode(); + +private: + void clearPulling(); + void pullQrcode(); + void showError(const QString &message); + + Ui::QiliLauncher *ui; + QiliHttp *mHttp; + + bool mAuthenticated{false}; + bool mRemember{true}; + int mQrTTL{0}; + QString mQrKey; + int mQrTimer{0}; + QPointF mStart{0, 0}; + QPoint mPos{0, 0}; +}; + +#endif // QILILOGIN_H diff --git a/App/QiliLogin.ui b/App/QiliLogin.ui new file mode 100644 index 0000000..d67d04b --- /dev/null +++ b/App/QiliLogin.ui @@ -0,0 +1,426 @@ + + + QiliLauncher + + + + 0 + 0 + 467 + 303 + + + + + :/images/qili.png:/images/qili.png + + + + + + + + 0 + 200 + 138 + 100 + + + + + + + :/images/bili-logo.svg + + + + + + 10 + 200 + 451 + 22 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + You can find it on the broadcasting page + + + + + + :/images/help.svg + + + + + + + If you start as anonymous, visitor's name in the room will not be shadowed + + + You can start withou scanning, just as anonymous + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 10 + 270 + 451 + 38 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + false + + + radio + + + + + + + If you start as anonymous, visitor's name in the room will not be shadowed + + + As Anonymous + + + anonymous + + + + + + + + + + + + + false + + + radio + + + + + + + Remember Me + + + rememberButton + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 10 + 220 + 451 + 51 + + + + + + + Qt::Horizontal + + + + 118 + 20 + + + + + + + + + 0 + 0 + + + + + 115 + 32 + + + + + 0 + 0 + + + + Start + + + primary + + + + + + + Qt::Horizontal + + + + 128 + 20 + + + + + + + + + + 0 + 0 + 465 + 35 + + + + Qili + + + titlebar + + + + + + 11 + 41 + 451 + 193 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 150 + 150 + + + + Click to refresh + + + + + + + :/images/qili.png:/images/qili.png + + + + 151 + 150 + + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 280 + 31 + + + + ROOM + + + + + + Please enter the number + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + QiliTextField + QWidget +
QiliTextField.h
+
+ + QiliTitleBar + QWidget +
QiliTitleBar.h
+
+
+ + + + + + qiliTitleBar + closing() + QiliLauncher + close() + + + 425 + 19 + + + 425 + 343 + + + + +
diff --git a/App/QiliProtocol.cpp b/App/QiliProtocol.cpp new file mode 100644 index 0000000..6106e65 --- /dev/null +++ b/App/QiliProtocol.cpp @@ -0,0 +1,190 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliProtocol.h" + +#include +#include + +// for logging purpose only +Q_DECLARE_FLAGS(BrotliDecoderResults, BrotliDecoderResult) +Q_DECLARE_OPERATORS_FOR_FLAGS(BrotliDecoderResults) + +const auto BROTLI_BUFFER_SIZE = 256; +QByteArray decode_brotli_compressed(const QByteArray &compressed) +{ + auto *decoder = BrotliDecoderCreateInstance(nullptr, nullptr, nullptr); + if (decoder == nullptr) { + qCritical() << "Can't create brotli decoder instance "; + return QByteArray(); + } + QByteArray decompressed; + + size_t available_in = compressed.size(); + const uint8_t *next_in = reinterpret_cast(compressed.constData()); + + size_t available_out; + uint8_t *next_out; + + BrotliDecoderResult result; + std::array buffer; + while (true) { + available_out = buffer.size(); + next_out = buffer.data(); + result = BrotliDecoderDecompressStream(decoder, &available_in, &next_in, &available_out, &next_out, nullptr); + decompressed.append(reinterpret_cast(buffer.data()), buffer.size() - available_out); + if (result == BROTLI_DECODER_RESULT_ERROR || + result == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT || + available_in == 0 && result == BROTLI_DECODER_RESULT_SUCCESS) { + break; + } + } + if (result != BROTLI_DECODER_RESULT_SUCCESS) { + qWarning() << "BROTLI_DECODER FAILED: result = " << result; + qWarning() << "BROTLI_DECODER FAILED: compressed = " << compressed; + } + + BrotliDecoderDestroyInstance(decoder); + return decompressed; +} + +QByteArray Codec::encode(int opcode, const QByteArray &body, BodyType bodyType) +{ + QByteArray bytes; + QDataStream io(&bytes, QIODevice::WriteOnly); + io.setByteOrder(QDataStream::BigEndian); + + qint32 bodySize = body.size(); + + qint16 header = 16; + qint32 length = header + bodySize; + qint16 type = bodyType; + qint32 operation = opcode; + qint32 sequence = 0; + + io << length; + io << header; + io << type; + io << operation; + io << sequence; + io.writeRawData(body.constData(), bodySize); + return bytes; +} + +void Codec::decode(const QByteArray &bytes, QList &packs) +{ + QDataStream io((QByteArray *) &bytes, QIODevice::ReadOnly); + io.setByteOrder(QDataStream::BigEndian); + decode(io, packs); +} + +QList Codec::decode(const QByteArray &bytes) +{ + QList packs; + decode(bytes, packs); + return packs; +} + +void Codec::decode(QDataStream &io, QList &packs) +{ + qint32 length = 0; + qint16 header = 0; + qint16 type = 0; + qint32 operation = 0; + qint32 sequence = 0; + + io >> length; + io >> header; + io >> type; + io >> operation; + io >> sequence; + + qDebug() << "Received { length = " << length + << ", header = " << header + << ", type = " << type + << ", operation = " << operation + << ", sequence = " << sequence + << "}"; + + if (operation == OP_HEARTBEAT_REPLY) { + qint32 watchers = 0; + int remaining = io.device()->bytesAvailable(); + if (remaining >= 4) { + io >> watchers; + remaining -= 4; + } + qInfo() << "Received Heartbeat: " << watchers; + packs << QJsonObject { + {"op", OP_HEARTBEAT_REPLY}, + {"cmd", "QILI_HEARTBEAT_REPLY"}, + {"watchers", watchers} + }; + + if (remaining > 0) { + qWarning() << "HeartBeat: " << remaining << " bytes after body"; + io.skipRawData(remaining); + } + return; + } + + int bodySize = length - header; + QByteArray raw; + raw.resize(bodySize); + io.readRawData(raw.data(), raw.size()); + QByteArray decompressed; + switch(type) { + case BROTLI_DATA: + qDebug() << "Decode Compressed BROTLI Body..."; + decompressed = decode_brotli_compressed(raw); + qDebug() << "Decode Compressed BROTLI Recursively: START"; + decode(decompressed, packs); + qDebug() << "Decode Compressed BROTLI Recursively: END"; + return; + case ZLIB_DATA: + qDebug() << "Decode Compressed ZLIB Body..."; + decompressed = qUncompress(raw); + qDebug() << "Decode Compressed ZLIB Recursize: START"; + decode(decompressed, packs); + qDebug() << "Decode Compressed ZLIB Recursize: END"; + return; + case PLAIN_DATA: + case PLAIN_AUTH: + qDebug() << "Received NONE compressed data"; + decompressed = raw; + break; + default: + qWarning() << "Unknow body: type = " << type; + qWarning() << "Unknow body: data = " << raw; + return; + } + + QJsonParseError state; + // qDebug() << "Body Bytes = " << decompressed; + QJsonDocument json = QJsonDocument::fromJson(decompressed, &state); + if (state.error != QJsonParseError::NoError) { + qWarning() << "Can't parse body json: " << state.errorString(); + return; + } + QJsonObject body = json.object(); + // qDebug() << "Body: " << QJsonDocument(body).toJson(QJsonDocument::Indented); + body["op"] = operation; + packs << body; + + if (!io.atEnd()) { + qDebug() << "More bytes avalilable: " << io.device()->bytesAvailable() << ", sequencially decoding..."; + decode(io, packs); + } +} diff --git a/App/QiliProtocol.h b/App/QiliProtocol.h new file mode 100644 index 0000000..93ed164 --- /dev/null +++ b/App/QiliProtocol.h @@ -0,0 +1,82 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILIPROTOCOL_H +#define QILIPROTOCOL_H + +#include "QiliAppGlobal.h" + +#include +#include +#include +#include +#include +#include +#include + +enum Opcode { + OP_HEARTBEAT = 2, // 客户端发送的心跳包(30秒发送一次) + OP_HEARTBEAT_REPLY = 3, //服务器收到心跳包的回复 + OP_SEND_SMS_REPLY = 5, //服务器推送的弹幕消息包 + OP_AUTH = 7, //客户端发送的鉴权包(客户端发送的第一个包) + OP_AUTH_REPLY = 8 //服务器收到鉴权包后的回复 +}; + +enum BodyType { + PLAIN_DATA = 0, + PLAIN_AUTH = 1, + ZLIB_DATA = 2, + BROTLI_DATA = 3 +}; + +namespace Commands +{ + // 直播间内用户排名 + constexpr const char * const ONLINE_RANK_V2 = "ONLINE_RANK_V2"; + // 弹幕 + constexpr const char * const DANMU_MSG = "DANMU_MSG"; + // 几人看过 + constexpr const char * const WATCHED_CHANGE = "WATCHED_CHANGE"; + +}; + +class QILI_APP_EXPORT Codec +{ +public: + + static QByteArray encode(int opcode, BodyType bodyType = BodyType::PLAIN_DATA) + { + return encode(opcode, QByteArray()); + } + + static QByteArray encode(int opcode, const QJsonObject &data, BodyType bodyType = BodyType::PLAIN_DATA) + { + QByteArray body = QJsonDocument(data).toJson(QJsonDocument::Compact); + return encode(opcode, body, bodyType); + } + + static QByteArray encode(int opcode, const QByteArray &body, BodyType bodyType = BodyType::PLAIN_DATA); + + static QList decode(const QByteArray &bytes); + +private: + Codec() = delete; + + static void decode(const QByteArray &bytes, QList &packs); + static void decode(QDataStream &io, QList &packs); +}; + +#endif // QILIPROTOCOL_H diff --git a/App/QiliSettings.cpp b/App/QiliSettings.cpp new file mode 100644 index 0000000..2dd11c0 --- /dev/null +++ b/App/QiliSettings.cpp @@ -0,0 +1,124 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliSettings.h" + +#include "QiliGlobal.h" + +#include + +using namespace Qili; + +QiliSettings::QiliSettings(QObject *parent) : + QObject(parent), + mSettings(new QSettings) +{} + +QiliSettings::~QiliSettings() +{ + delete mSettings; +} + +bool QiliSettings::keepUser() +{ + return mSettings->value(SettingKeys::KeepUser, true).toBool(); +} + +bool QiliSettings::keepRoom() +{ + auto value = mSettings->value(SettingKeys::KeepRoom, true).toBool(); + return value; +} + +int QiliSettings::room() +{ + return mSettings->value(SettingKeys::Room, 0).toInt(); +} + +QByteArray QiliSettings::cookies() +{ + return mSettings->value(SettingKeys::Cookies, QByteArray()).toByteArray(); +} + +QLocale QiliSettings::speakerLocale() const +{ + return mSettings->value(SettingKeys::Lang, toStorable(QLocale::system())).value(); +} + +QString QiliSettings::speakerVoice() const +{ + return mSettings->value(SettingKeys::Voice, "").toString(); +} + +int QiliSettings::speakerVolume() const +{ + return mSettings->value(SettingKeys::Volume, 0).toInt(); +} + +double QiliSettings::speakerPitch() const +{ + return mSettings->value(SettingKeys::Pitch, 0.0).toDouble(); +} + +QLocale QiliSettings::toStorable(const QLocale &locale) +{ + return QLocale(locale.language(), locale.script(), QLocale::AnyCountry); +} + +void QiliSettings::setKeepUser(bool value) +{ + mSettings->setValue(SettingKeys::KeepUser, value); +} + +void QiliSettings::setKeepRoom(bool value) +{ + mSettings->setValue(SettingKeys::KeepRoom, value); +} + +void QiliSettings::setRoom(int value) +{ + mSettings->setValue(SettingKeys::Room, value); +} + +void QiliSettings::setCookies(const QByteArray &value) +{ + mSettings->setValue(SettingKeys::Cookies, value); +} + +void QiliSettings::removeCookies() +{ + mSettings->remove(SettingKeys::Cookies); +} + +void QiliSettings::setSpeakerLocale(const QLocale &value) +{ + mSettings->setValue(SettingKeys::Lang, toStorable(value)); +} + +void QiliSettings::setSpeakerVoice(const QString &value) +{ + mSettings->setValue(SettingKeys::Voice, value); +} + +void QiliSettings::setSpeakerVolume(int value) +{ + mSettings->setValue(SettingKeys::Volume, value); +} + +void QiliSettings::setSpeakerPitch(double value) +{ + mSettings->setValue(SettingKeys::Pitch, value); +} diff --git a/App/QiliSettings.h b/App/QiliSettings.h new file mode 100644 index 0000000..2cfaba2 --- /dev/null +++ b/App/QiliSettings.h @@ -0,0 +1,68 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILISETTINGS_H +#define QILISETTINGS_H + +#include "QiliAppGlobal.h" + +#include + +class QILI_APP_EXPORT QiliSettings : public QObject +{ + Q_OBJECT +public: + explicit QiliSettings(QObject *parent = nullptr); + ~QiliSettings(); + + bool keepUser(); + bool keepRoom(); + int room(); + QByteArray cookies(); + + QLocale speakerLocale() const; + QString speakerVoice() const; + int speakerVolume() const; + double speakerPitch() const; + + static QLocale toStorable(const QLocale &locale); + +public slots: + void setKeepUser(bool value); + void setKeepRoom(bool value); + void setRoom(int value); + void setCookies(const QByteArray &value); + void removeCookies(); + void setSpeakerLocale(const QLocale &value); + void setSpeakerVoice(const QString &value); + void setSpeakerVolume(int value); + void setSpeakerPitch(double value); + +private: + QSettings *mSettings; + /** + * auto system = mSpeaker->narrowed(QLocale::system()); + mSelectedLang = mSettings->value(SettingKeys::Lang, system).value(); + mSelectedVoice = mSettings->value(SettingKeys::Voice).toString(); + auto volume = mSettings->value(SettingKeys::Volume, 0).toInt(); + auto pitch = mSettings->value(SettingKeys::Pitch, 0.0).toDouble(); +*/ + + + +}; + +#endif // QILISETTINGS_H diff --git a/App/QiliSettingsDialog.cpp b/App/QiliSettingsDialog.cpp new file mode 100644 index 0000000..b9f1203 --- /dev/null +++ b/App/QiliSettingsDialog.cpp @@ -0,0 +1,218 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliSettingsDialog.h" +#include "ui_QiliSettingsDialog.h" + +#include "QiliSettings.h" + +#include +#include + + +QiliSettingsDialog::QiliSettingsDialog(QiliSettings *settings, QiliSpeaker *speaker, QWidget *parent) + : QiliDialog(parent) + , ui(new Ui::QiliSettingsDialog) + , mSettings(settings) + , mSpeaker(new QiliSpeaker()) +{ + ui->setupUi(this); + // basic tab + QObject::connect(ui->userRadio, &QRadioButton::clicked, this, &QiliSettingsDialog::onUserRadioClicked); + QObject::connect(ui->roomRadio, &QRadioButton::clicked, this, &QiliSettingsDialog::onRoomRadioClicked); + + // voice tab + QObject::connect(ui->languageCombo, qOverload(&QComboBox::currentIndexChanged), this, &QiliSettingsDialog::onLanguageChanged); + QObject::connect(ui->volumeSpin, qOverload(&QSpinBox::valueChanged), this, &QiliSettingsDialog::onVolumeSpinChanged); + QObject::connect(ui->pitchSpin, qOverload(&QDoubleSpinBox::valueChanged), this, &QiliSettingsDialog::onPitchSpinChanged); + QObject::connect(ui->voicesCombo, qOverload(&QComboBox::currentIndexChanged), this, &QiliSettingsDialog::onVoiceChanged); + QObject::connect(ui->volumeSlider, qOverload(&QSlider::valueChanged), this, &QiliSettingsDialog::onVolumeSliderChanged); + QObject::connect(ui->volumeSpin, qOverload(&QSpinBox::valueChanged), this, &QiliSettingsDialog::onVolumeSpinChanged); + QObject::connect(ui->pitchSlider, qOverload(&QSlider::valueChanged), this, &QiliSettingsDialog::onPitchSliderChanged); + QObject::connect(ui->pitchSpin, qOverload(&QDoubleSpinBox::valueChanged), this, &QiliSettingsDialog::onPitchSpinChanged); + + QObject::connect(ui->testButton, &QRadioButton::clicked, this, &QiliSettingsDialog::onTestButtonClicked); + QObject::connect(ui->applyButton, &QPushButton::clicked, this, &QiliSettingsDialog::onApplyButtonClicked); +} + +QiliSettingsDialog::~QiliSettingsDialog() +{ + delete ui; + delete mSpeaker; +} + +void QiliSettingsDialog::show() +{ + mLanguages = mSpeaker->avaiableLanguages(); + mSelectedLang = mSettings->speakerLocale(); + mSelectedVoice = mSettings->speakerVoice(); + auto volume = mSettings->speakerVolume(); + auto pitch = mSettings->speakerPitch(); + + ui->volumeSpin->setValue(volume); + ui->pitchSpin->setValue(pitch); + ui->roomRadio->setChecked(mSettings->keepRoom()); + ui->userRadio->setChecked(mSettings->keepUser()); + + mReset = true; + setupLanguages(); + mReset = false; + + QiliDialog::show(); +} + +void QiliSettingsDialog::setupLanguages() +{ + ui->languageCombo->clear(); + // fill languages to the combox + auto index = 0, selected = index; + for (const auto &lang : mLanguages) { + if (mReset && lang == mSelectedLang) { + selected = index; + } + ui->languageCombo->addItem(lang.nativeLanguageName(), lang); + index++; + } + ui->languageCombo->setCurrentIndex(selected); +} + +void QiliSettingsDialog::setupVoices() +{ + ui->voicesCombo->clear(); + + auto index = 0, selected = index; + for (const auto &voice : std::as_const(mVoices)) { + if (mReset && voice.name() == mSelectedVoice) { + selected = index; + } +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + ui->voicesCombo->addItem(voice.name(), QVariant(std::in_place_type_t(), voice)); +#else + QVariant data; + data.setValue(voice); + ui->voicesCombo->addItem(voice.name(), data); +#endif + index++; + } + + ui->voicesCombo->setCurrentIndex(selected); +} + +void QiliSettingsDialog::onLanguageChanged(int index) +{ + ui->voicesCombo->clear(); + + if (index == -1) { + return; + } + const auto lang = ui->languageCombo->itemData(index).value(); + mSpeaker->setLocale(lang); + mVoices = mSpeaker->avaiableVoices(lang); + setupVoices(); +} + +void QiliSettingsDialog::onVoiceChanged(int index) +{ + if (index == -1) { + return; + } + const auto voice = ui->voicesCombo->itemData(index).value(); + mSpeaker->setVoice(voice); +} + +void QiliSettingsDialog::onVolumeSpinChanged(int value) +{ + mSpeaker->setVolume(value); + if (value != ui->volumeSlider->value()) { + ui->volumeSlider->setValue(value); + } +} + +void QiliSettingsDialog::onVolumeSliderChanged(int value) +{ + if (value != ui->volumeSpin->value()) { + ui->volumeSpin->setValue(value); + } +} + +void QiliSettingsDialog::onPitchSpinChanged(double value) +{ + mSpeaker->setPitch(value); + auto v1 = (int)(value * 10); + if (v1 != ui->pitchSlider->value()) { + ui->pitchSlider->setValue(v1); + } +} + +void QiliSettingsDialog::onPitchSliderChanged(int value) +{ + auto v1 = value / 10.0; + if (v1 != ui->pitchSpin->value()) { + ui->pitchSpin->setValue(v1); + } +} + +void QiliSettingsDialog::onUserRadioClicked(bool checked) +{ + if (checked) { + mSettings->setKeepUser(true); + auto button = QMessageBox::question(this, tr("Qili"), tr("Do you want login bilibili.com now?")); + if (button == QMessageBox::Yes) { + emit restart(); + } + } + else { + auto button = QMessageBox::question(this, tr("Qili"), tr("Are you sure to clear the authentication for bilibili.com?")); + if (button == QMessageBox::Yes) { + mSettings->removeCookies(); + mSettings->setKeepUser(false); + emit restart(); + } + else { + ui->userRadio->setChecked(true); + } + } +} + +void QiliSettingsDialog::onRoomRadioClicked(bool checked) +{ + if (!checked) { + auto button = QMessageBox::question(this, tr("Qili"), tr("Are you sure to clear the stored room number?")); + if (button == QMessageBox::Yes) { + mSettings->setRoom(0); + } else { + ui->roomRadio->setChecked(true); + } + } +} + +void QiliSettingsDialog::onTestButtonClicked() +{ + auto text = ui->testText->text().trimmed(); + if (!text.isEmpty()) { + mSpeaker->speak(text); + } +} + +void QiliSettingsDialog::onApplyButtonClicked() +{ + auto lang = ui->languageCombo->currentData().value(); + auto voice = ui->voicesCombo->currentData().value(); + auto volume = ui->volumeSpin->value(); + auto pitch = ui->pitchSpin->value(); + + emit apply(lang, voice, volume, pitch); +} diff --git a/App/QiliSettingsDialog.h b/App/QiliSettingsDialog.h new file mode 100644 index 0000000..0cc5c19 --- /dev/null +++ b/App/QiliSettingsDialog.h @@ -0,0 +1,75 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILISETTINGSDIALOG_H +#define QILISETTINGSDIALOG_H + +#include "QiliAppGlobal.h" +#include "QiliSpeaker.h" +#include "QiliDialog.h" + +#include +#include + +namespace Ui { +class QiliSettingsDialog; +} + +class QiliSettings; + +class QILI_APP_EXPORT QiliSettingsDialog : public QiliDialog +{ + Q_OBJECT + +public: + explicit QiliSettingsDialog(QiliSettings *settings, QiliSpeaker *speaker, QWidget *parent = nullptr); + ~QiliSettingsDialog(); + + void setupLanguages(); + + void setupVoices(); + +signals: + void restart(); + void apply(const QLocale &locale, const QVoice &voice, int volume, double pitch); + +public slots: + void show(); + +private slots: + void onLanguageChanged(int index); + void onVoiceChanged(int index); + void onVolumeSpinChanged(int value); + void onVolumeSliderChanged(int value); + void onPitchSpinChanged(double value); + void onPitchSliderChanged(int value); + void onUserRadioClicked(bool checked); + void onRoomRadioClicked(bool checked); + void onTestButtonClicked(); + void onApplyButtonClicked(); + +private: + Ui::QiliSettingsDialog *ui; + QiliSpeaker *mSpeaker; + QiliSettings *mSettings; + QList mLanguages; + QList mVoices; + QString mSelectedVoice; + QLocale mSelectedLang; + bool mReset{true}; +}; + +#endif // QILISETTINGSDIALOG_H diff --git a/App/QiliSettingsDialog.ui b/App/QiliSettingsDialog.ui new file mode 100644 index 0000000..5a3e97d --- /dev/null +++ b/App/QiliSettingsDialog.ui @@ -0,0 +1,403 @@ + + + QiliSettingsDialog + + + + 0 + 0 + 530 + 390 + + + + + 0 + 0 + + + + Qili Settings + + + + :/images/qili.png:/images/qili.png + + + + 8 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Qili Settings + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 13 + 20 + + + + + + + + 1 + + + + false + + + Basic + + + + + + Remember Room + + + roomRadio + + + + + + + Remember User + + + userRadio + + + + + + + Qt::Vertical + + + + 20 + 233 + + + + + + + + Qt::Vertical + + + + 20 + 233 + + + + + + + + + + + false + + + radio + + + + + + + + + + false + + + radio + + + + + + + + + 0 + 0 + + + + Voices + + + + + + Language + + + languageCombo + + + + + + + + + + Voice + + + voicesCombo + + + + + + + + + + Volume + + + volumeSpin + + + + + + + 0 + + + 100 + + + Qt::Horizontal + + + + + + + 0 + + + 100 + + + + + + + Pitch + + + pitchSpin + + + + + + + 0 + + + 20 + + + 20 + + + Qt::Horizontal + + + + + + + 1 + + + 0.000000000000000 + + + 2.000000000000000 + + + 0.200000000000000 + + + + + + + This is the text to speech + + + + + + + Test + + + primary + + + + + + + Qt::Vertical + + + + 20 + 89 + + + + + + + + Qt::Horizontal + + + + 131 + 20 + + + + + + + + Apply + + + primary + + + + + + + Qt::Horizontal + + + + 150 + 20 + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 13 + 20 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 3 + + + + + + + + + QiliTitleBar + QWidget +
QiliTitleBar.h
+
+
+ + + + + + qiliTitleBar + closing() + QiliSettingsDialog + close() + + + 430 + 16 + + + 439 + 356 + + + + +
diff --git a/App/QiliSocket.cpp b/App/QiliSocket.cpp new file mode 100644 index 0000000..1ba876e --- /dev/null +++ b/App/QiliSocket.cpp @@ -0,0 +1,159 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliSocket.h" + +#include "QiliProtocol.h" +#include "QiliGlobal.h" +#include "Utility.h" + +#include +#include +#include +#include + +using namespace Utility::JSON; + + +QiliSocket::QiliSocket(QObject *parent) + : QObject(parent) +{ + mSocket = new QWebSocket(QString(), QWebSocketProtocol::Version13); + // mSocket->ignoreSslErrors(); + QObject::connect(mSocket, &QWebSocket::connected, this, &QiliSocket::authenticate); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QObject::connect(mSocket, QOverload::of(&QWebSocket::errorOccurred), this, &QiliSocket::handleError); +#else + QObject::connect(mSocket, QOverload::of(&QWebSocket::error), this, &QiliSocket::handleError); +#endif + QObject::connect(mSocket, &QWebSocket::binaryMessageReceived, this, &QiliSocket::receivedBinaryMessage); + + mTimer = new QTimer(this); + mTimer->setInterval(20000); + QObject::connect(mTimer, &QTimer::timeout, this, &QiliSocket::heartbeat); +} + +QiliSocket::~QiliSocket() +{ + // delete mSocket; + // delete mTimer; +} + +void QiliSocket::authenticate() +{ + qDebug() << "Connected, Now authicating with: " << mAuth; + QByteArray data = mAuth.toUtf8(); + mSocket->sendBinaryMessage(Codec::encode(OP_AUTH, data, BodyType::PLAIN_AUTH)); +} + +void QiliSocket::heartbeat() +{ + qDebug() << "heartbeat: time = " << QDateTime::currentDateTime() + << ",state = " << mSocket->state(); + // It seems that the client side shoud not check heartbeat status + if (Qili::Released) { // enabled ONLY if in released binary + // if can't receive heartbeat reply in 3 cycle, we should close the connection + qint64 now = QDateTime::currentSecsSinceEpoch(); + if (!mSocket->isValid() || mLastHBAt > 0 && now - mLastHBAt > 60) { + emit errorOccured("Socket goes invalid"); + return; + } + } + mSocket->sendBinaryMessage(Codec::encode(OP_HEARTBEAT, BodyType::PLAIN_DATA)); +} + +void QiliSocket::receivedHeartbeatReply(const QJsonObject &pack) +{ + mLastHBAt = QDateTime::currentSecsSinceEpoch(); + qDebug() << "receivedHeartbeatReply: " << pack; + int count = pack / "watchers" >> JInt; + emit watchersChanged(count); +} + +void QiliSocket::receivedAuthReply(const QJsonObject &pack) +{ + int code = pack / "code" >> JInt; + qDebug() << "receivedAuthReply: pack = " << pack; + qDebug() << "receivedAuthReply: code = " << code; + if (code == 0) { + // heartbeat(); + mTimer->start(); + emit authenticated(); + } else { + qDebug() << "receivedAuthReply: failed = " << pack; + emit errorOccured("Auth failed"); + } +} + +void QiliSocket::receivedSmsReply(const QJsonObject &pack) +{ + qDebug() << "receivedSmsReply: " << pack; + emit subtitleReceived(pack); +} + + +void QiliSocket::handleError(QAbstractSocket::SocketError error) +{ + qCritical() << "Error Occured: code = " << error; + qCritical() << "Error Occured: msg = " << mSocket->errorString(); + emit errorOccured(mSocket->errorString()); +} + +void QiliSocket::receivedBinaryMessage(const QByteArray &bytes) +{ + qDebug() << "Binary Received: size = " << bytes.size(); + QList packs = Codec::decode(bytes); + + for (const auto &pack : packs) { + int operation = pack / "op" >> JInt; + switch (operation) { + case OP_HEARTBEAT_REPLY: + receivedHeartbeatReply(pack); + break; + case OP_AUTH_REPLY: + receivedAuthReply(pack); + break; + case OP_SEND_SMS_REPLY: + receivedSmsReply(pack); + break; + default: + qWarning() << "Received Unknown Pack: " << pack; + } + } +} + +void QiliSocket::open() +{ + QNetworkRequest request(mUrl); + request.setHeader(QNetworkRequest::UserAgentHeader, Qili::UserAgent); + qDebug() << "Connecting to: " << mUrl; + mSocket->open(mUrl); +} + +void QiliSocket::close() +{ + mTimer->stop(); + mSocket->abort(); +} + +void QiliSocket::setAuth(const QString &auth) +{ + mAuth = auth; +} +void QiliSocket::setUrl(const QString &url) +{ + mUrl = url; +} diff --git a/App/QiliSocket.h b/App/QiliSocket.h new file mode 100644 index 0000000..3792596 --- /dev/null +++ b/App/QiliSocket.h @@ -0,0 +1,66 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILISOCKET_H +#define QILISOCKET_H + +#include "QiliAppGlobal.h" +#include "QiliProtocol.h" + +#include +#include +#include + +class QILI_APP_EXPORT QiliSocket : public QObject +{ + Q_OBJECT +public: + explicit QiliSocket(QObject *parent = nullptr); + ~QiliSocket(); + +signals: + void authenticated(); + void errorOccured(const QString &error); + void subtitleReceived(const QJsonObject &sub); + void watchersChanged(int count); + +public slots: + void open(); + void setAuth(const QString &auth); + void setUrl(const QString &url); + void close(); + +private slots: + void authenticate(); + void heartbeat(); + + void handleError(QAbstractSocket::SocketError error); + void receivedBinaryMessage(const QByteArray &bytes); + void receivedHeartbeatReply(const QJsonObject &pack); + void receivedAuthReply(const QJsonObject &pack); + void receivedSmsReply(const QJsonObject &pack); + +private: + QString mAuth; + QString mUrl; + + QWebSocket *mSocket; + + QTimer *mTimer; + qint64 mLastHBAt{0}; +}; + +#endif // QILISOCKET_H diff --git a/App/QiliSpeaker.cpp b/App/QiliSpeaker.cpp new file mode 100644 index 0000000..60a0153 --- /dev/null +++ b/App/QiliSpeaker.cpp @@ -0,0 +1,211 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliSpeaker.h" + +#include "QiliSettings.h" + +#include +#include +#include + + +const QList QiliSpeaker::SupportedLanguages = { + QLocale(QLocale::English, QLocale::UnitedStates), + QLocale(QLocale::Chinese, QLocale::SimplifiedChineseScript, QLocale::AnyCountry) +}; + + +QiliSpeaker::QiliSpeaker(QObject *parent) + : QObject(parent) +{ + mTextToSpeech = new QTextToSpeech(this); +} + +QiliSpeaker::~QiliSpeaker() +{} + +bool QiliSpeaker::isLanguageSupported(const QLocale &locale) { + for (const auto &l : std::as_const(SupportedLanguages)) { + if (l.language() == locale.language() && (l.script() == QLocale::AnyScript || l.script() == locale.script())) { + return true; + } + } + return false; +} + +void QiliSpeaker::restore(const QiliSettings &settings) +{ + auto lang = settings.speakerLocale(); + auto voice = settings.speakerVoice(); + auto volume = settings.speakerVolume(); + auto pitch = settings.speakerPitch(); + mTextToSpeech->setLocale(lang); + mTextToSpeech->setVolume(volume); + mTextToSpeech->setPitch(pitch); + if (!voice.isEmpty()) { + auto voices = avaiableVoices(lang); + auto found = std::find_if(voices.cbegin(), voices.cend(), [&](const QVoice &it) -> bool { + return it.name() == voice; + }); + if (found != voices.cend()) { + mTextToSpeech->setVoice(*found); + } + } +} + + +void QiliSpeaker::setLocale(const QLocale &locale) +{ + mTextToSpeech->setLocale(locale); +} + +QList QiliSpeaker::avaiableLanguages() +{ + auto clocale = QLocale::c(); + auto system = QLocale::system(); + auto locales = mTextToSpeech->availableLocales(); + QSet distincted; + distincted += QiliSettings::toStorable(system); + distincted += QiliSettings::toStorable(QLocale(QLocale::Chinese, QLocale::SimplifiedChineseScript, QLocale::AnyCountry)); + for (const auto &locale : std::as_const(locales)) { + if (locale == clocale || !isLanguageSupported(locale)) { + continue; + } + distincted += QiliSettings::toStorable(locale); + } + return QList(distincted.begin(), distincted.end()); +} + +QList QiliSpeaker::avaiableVoices(const QLocale &language) +{ + const auto langName = QLocale::languageToString(language.language()); + const auto cLocale = QLocale::c(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QList voices = mTextToSpeech->findVoices(QLocale::C); + + auto currentLocale = mTextToSpeech->locale(); + + for (const auto &supportedLanguage : std::as_const(SupportedLanguages)) { + mTextToSpeech->setLocale(supportedLanguage); + auto localized = mTextToSpeech->findVoices(supportedLanguage.language()); + voices += localized; + } + mTextToSpeech->setLocale(currentLocale); + + QList avaiables; + for (const auto &voice : std::as_const(voices)) { + auto locale = QiliSettings::toStorable(voice.locale()); + bool supported = false; + for (const auto &supportedLocale : std::as_const(SupportedLanguages)) { + const auto &name = voice.name(); + if ((locale == cLocale && name.startsWith(langName)) + || (supportedLocale.language() == locale.language() + && (supportedLocale.script() == QLocale::AnyScript + || supportedLocale.script() == locale.script() + || name.startsWith(langName)) + && (language.script() == QLocale::AnyScript + || language.script() == locale.script() + || name.startsWith(langName))) + ) { + supported = true; + break; + } + } + if (supported) { + avaiables += voice; + } + } + return avaiables; +#else + auto originalLocale = mTextToSpeech->locale(); + mTextToSpeech->setLocale(language); + QVector voices = mTextToSpeech->availableVoices(); + mTextToSpeech->setLocale(QLocale::c()); + voices += mTextToSpeech->availableVoices(); + QList avaiables; + for (const auto &voice : std::as_const(voices)) { + if(voice.name().contains(langName)) { + avaiables += voice; + } + } + mTextToSpeech->setLocale(originalLocale); + return avaiables; +#endif +} + +QLocale QiliSpeaker::locale() const +{ + return mTextToSpeech->locale(); +} + +QVoice QiliSpeaker::voice() const +{ + return mTextToSpeech->voice(); +} + + +void QiliSpeaker::setVoice(const QVoice &voice) +{ + mTextToSpeech->setVoice(voice); +} + +void QiliSpeaker::setVolume(int volume) +{ + mTextToSpeech->setVolume(volume / 100.0); +} + +int QiliSpeaker::volume() const +{ + auto volume = mTextToSpeech->volume(); + return (int) (volume * 100); +} + +double QiliSpeaker::pitch() const +{ + return mTextToSpeech->pitch() + 1; +} + +void QiliSpeaker::setPitch(double pitch) +{ + mTextToSpeech->setPitch(pitch - 1); +} + +void QiliSpeaker::speak(const QString &text) +{ + qDebug() << "Speaking: " << qUtf8Printable(text); + mTextToSpeech->say(text); +} + +bool QiliSpeaker::supportPause() const +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return mTextToSpeech->engineCapabilities() & QTextToSpeech::Capability::PauseResume; +#else + // Qt5 does not provide an api to detect it, let's assume it as supported + return true; +#endif +} + +void QiliSpeaker::pause() +{ + mTextToSpeech->pause(); +} + +void QiliSpeaker::resume() +{ + mTextToSpeech->resume(); +} diff --git a/App/QiliSpeaker.h b/App/QiliSpeaker.h new file mode 100644 index 0000000..db73f5c --- /dev/null +++ b/App/QiliSpeaker.h @@ -0,0 +1,66 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILISPEAKER_H +#define QILISPEAKER_H + +#include "QiliAppGlobal.h" + +#include +#include +#include +#include +#include + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +Q_DECLARE_METATYPE(QVoice) +#endif + +class QiliSettings; + +class QILI_APP_EXPORT QiliSpeaker : public QObject +{ + Q_OBJECT +public: + explicit QiliSpeaker(QObject *parent = nullptr); + ~QiliSpeaker(); + + QList avaiableLanguages(); + QList avaiableVoices(const QLocale &language); + QLocale locale() const; + QVoice voice() const; + int volume() const; + double pitch() const; + void restore(const QiliSettings &settings); + +public slots: + void setLocale(const QLocale &locale); + void setVoice(const QVoice &voice); + void setVolume(int volume); + void setPitch(double pitch); + void speak(const QString &text); + bool supportPause() const; + void pause(); + void resume(); + +private: + static const QList SupportedLanguages; + bool isLanguageSupported(const QLocale &locale); + + QTextToSpeech *mTextToSpeech; +}; + +#endif // QILISPEAKER_H diff --git a/App/QiliSubtitleLogger.cpp b/App/QiliSubtitleLogger.cpp new file mode 100644 index 0000000..0a1ebe4 --- /dev/null +++ b/App/QiliSubtitleLogger.cpp @@ -0,0 +1,66 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliSubtitleLogger.h" +#include "ui_QiliSubtitleLogger.h" + +#include +#include + + +QiliSubtitleLogger::QiliSubtitleLogger(QWidget *parent) : + QiliDialog(parent), + ui(new Ui::QiliSubtitleLogger) +{ + ui->setupUi(this); + ui->subtitles->clear(); + auto cursor = ui->subtitles->textCursor(); + cursor.movePosition(QTextCursor::Start); + cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); + cursor.select(QTextCursor::BlockUnderCursor); + cursor.removeSelectedText(); +} + +QiliSubtitleLogger::~QiliSubtitleLogger() +{ + delete ui; +} + +void QiliSubtitleLogger::subtitleReceived(const QString &message) +{ + if (mMaxLines == 1) { + ui->subtitles->clear(); + } + int lines = ui->subtitles->document()->lineCount() - 1; + if (lines >= mMaxLines) { + auto cursor = ui->subtitles->textCursor(); + cursor.movePosition(QTextCursor::End); + cursor.movePosition(QTextCursor::Up, QTextCursor::KeepAnchor); + cursor.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor); + cursor.select(QTextCursor::BlockUnderCursor); + qDebug() << "Remove LINE:" << cursor.selectedText(); + cursor.removeSelectedText(); + cursor.clearSelection(); + } + ui->subtitles->moveCursor(QTextCursor::Start); + ui->subtitles->insertPlainText(message); + ui->subtitles->insertPlainText("\n"); +} + +void QiliSubtitleLogger::setMaxLines(int maxLines) +{ + mMaxLines = maxLines; +} diff --git a/App/QiliSubtitleLogger.h b/App/QiliSubtitleLogger.h new file mode 100644 index 0000000..c413bbc --- /dev/null +++ b/App/QiliSubtitleLogger.h @@ -0,0 +1,48 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILISUBTITLELOGGER_H +#define QILISUBTITLELOGGER_H + +#include "QiliAppGlobal.h" +#include "QiliDialog.h" + +#include + +namespace Ui { +class QiliSubtitleLogger; +} + +class QILI_APP_EXPORT QiliSubtitleLogger : public QiliDialog +{ + Q_OBJECT + +public: + explicit QiliSubtitleLogger(QWidget *parent = nullptr); + ~QiliSubtitleLogger(); + + int maxLines() const; + +public slots: + void subtitleReceived(const QString &message); + void setMaxLines(int maxLines); + +private: + int mMaxLines{100}; + Ui::QiliSubtitleLogger *ui; +}; + +#endif // QILISUBTITLELOGGER_H diff --git a/App/QiliSubtitleLogger.ui b/App/QiliSubtitleLogger.ui new file mode 100644 index 0000000..bc1fb66 --- /dev/null +++ b/App/QiliSubtitleLogger.ui @@ -0,0 +1,139 @@ + + + QiliSubtitleLogger + + + + 0 + 0 + 530 + 390 + + + + Subtitle Logs + + + + :/images/qili.png:/images/qili.png + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Subtitle Logs + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + QTextEdit::NoWrap + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + + + QiliTitleBar + QWidget +
QiliTitleBar.h
+
+
+ + + + + + qiliTitleBar + closing() + QiliSubtitleLogger + close() + + + 557 + 23 + + + 545 + 421 + + + + +
diff --git a/App/QiliThanksDialog.cpp b/App/QiliThanksDialog.cpp new file mode 100644 index 0000000..8b9895b --- /dev/null +++ b/App/QiliThanksDialog.cpp @@ -0,0 +1,30 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliThanksDialog.h" +#include "ui_QiliThanksDialog.h" + +QiliThanksDialog::QiliThanksDialog(QWidget *parent) + : QiliDialog(parent) + , ui(new Ui::QiliThanksDialog) +{ + ui->setupUi(this); +} + +QiliThanksDialog::~QiliThanksDialog() +{ + delete ui; +} diff --git a/App/QiliThanksDialog.h b/App/QiliThanksDialog.h new file mode 100644 index 0000000..07ba564 --- /dev/null +++ b/App/QiliThanksDialog.h @@ -0,0 +1,40 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILITHANKSDIALOG_H +#define QILITHANKSDIALOG_H + +#include "QiliDialog.h" + +#include + +namespace Ui { +class QiliThanksDialog; +} + +class QiliThanksDialog : public QiliDialog +{ + Q_OBJECT + +public: + explicit QiliThanksDialog(QWidget *parent = nullptr); + ~QiliThanksDialog(); + +private: + Ui::QiliThanksDialog *ui; +}; + +#endif // QILITHANKSDIALOG_H diff --git a/App/QiliThanksDialog.ui b/App/QiliThanksDialog.ui new file mode 100644 index 0000000..1c43329 --- /dev/null +++ b/App/QiliThanksDialog.ui @@ -0,0 +1,324 @@ + + + QiliThanksDialog + + + + 0 + 0 + 530 + 390 + + + + About Qili + + + + :/images/qili.png:/images/qili.png + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + About Qili + + + titlebar + + + + + + + An open source and free subtitle spearker for live broadcasting at bilibili.com + + + Qt::AlignCenter + + + true + + + + + + + Qt::Vertical + + + + 20 + 60 + + + + + + + + + + + true + + + + Thanks + + + Qt::AlignCenter + + + + + + + Sauntor <sauntor@live.com> (Author) + + + Qt::AlignCenter + + + + + + + + + Qt::Vertical + + + + 20 + 60 + + + + + + + + + + Qt::Vertical + + + + 20 + 5 + + + + + + + + + true + + + + Sponsor + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 5 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 200 + 200 + + + + + 128 + 128 + + + + + + + :/images/alipay.png + + + true + + + Qt::AlignCenter + + + + + + + Alipay + + + Qt::AlignCenter + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 200 + 200 + + + + + + + :/images/wechat.png + + + true + + + Qt::AlignCenter + + + + + + + Wechat + + + Qt::AlignCenter + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 5 + + + + + + + + + + + QiliTitleBar + QWidget +
QiliTitleBar.h
+
+
+ + + + + + + qiliTitleBar + closing() + QiliThanksDialog + close() + + + 453 + 16 + + + 433 + 394 + + + + +
diff --git a/App/QiliTray.cpp b/App/QiliTray.cpp new file mode 100644 index 0000000..a861fa2 --- /dev/null +++ b/App/QiliTray.cpp @@ -0,0 +1,348 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliTray.h" + +#include "QiliLogin.h" +#include "QiliGlobal.h" +#include "Utility.h" + +#include +#include +#include +#include +#include +#include + +using namespace Qili; +using namespace Utility::JSON; +using Utility::Speakable; + +QiliTray::QiliTray(QObject *parent) + : QSystemTrayIcon(parent) +{ + mHttp = new QiliHttp(); + mSettings = new QiliSettings(); + mLauncher = new QiliLauncher(mHttp); + mLauncher->installEventFilter(this); + mSubtitleLogger = new QiliSubtitleLogger(); + + mSpeaker = new QiliSpeaker(); + mSpeaker->restore(*mSettings); + + mSettingsDialog = new QiliSettingsDialog(mSettings, mSpeaker); + mThanksDialog = new QiliThanksDialog(); + + mConnectAction = new QAction(tr("Re&Connect")); + mLoggerAction = new QAction(tr("&Logger")); + mSettingsAction = new QAction(tr("&Settings")); + mThanksAction = new QAction(tr("&Thanks")); + mRestartAction = new QAction(tr("&Restart")); + mExitAction = new QAction(tr("&Exit")); + + menu = new QMenu(); + menu->addAction(mConnectAction); + menu->addAction(mLoggerAction); + menu->addAction(mSettingsAction); + menu->addSeparator(); + menu->addAction(mThanksAction); + menu->addSeparator(); + menu->addAction(mRestartAction); + menu->addAction(mExitAction); + + this->setContextMenu(menu); + + this->setToolTip(tr("Qili")); + this->setIcon(QIcon(":/images/qili.png")); + + int room = mSettings->room(); + mLauncher->setRoom(room); + + bool remember = mSettings->keepUser(); + mLauncher->setRemember(remember); + if (remember) { + mHttp->restoreCookies(mSettings->cookies()); + } + + QObject::connect(this, &QiliTray::activated, this, &QiliTray::onIconActived); + QObject::connect(mLoggerAction, &QAction::triggered, this, &QiliTray::onLoggerTriggered); + QObject::connect(mConnectAction, &QAction::triggered, this, &QiliTray::onConnectTriggered); + QObject::connect(mSettingsAction, &QAction::triggered, mSettingsDialog, &QiliSettingsDialog::show); + QObject::connect(mThanksAction, &QAction::triggered, this, &QiliTray::onThanksTriggered); + QObject::connect(mRestartAction, &QAction::triggered, this, &QiliTray::onRestart); + QObject::connect(mExitAction, &QAction::triggered, qApp, &QCoreApplication::exit); + + QObject::connect(mLauncher, &QiliLauncher::rememberChanged, this, &QiliTray::onRememberChanged); + QObject::connect(mLauncher, &QiliLauncher::starting, this, qOverload(&QiliTray::connect)); + auto expires = mHttp->expires(); + if (expires.isValid() && expires.toMSecsSinceEpoch() > QDateTime::currentMSecsSinceEpoch()) { + mLauncher->setAuthenticated(true); + } + + QObject::connect(mSettingsDialog, &QiliSettingsDialog::apply, this, &QiliTray::onVoiceApply); + QObject::connect(mSettingsDialog, &QiliSettingsDialog::restart, this, &QiliTray::onRestart); +} + +QiliTray::~QiliTray() +{ + delete mHttp; + delete mSettings; + delete mSettingsDialog; + delete mLauncher; + delete mSubtitleLogger; + delete mSpeaker; + delete menu; +} + +void QiliTray::show() +{ + if (started) { + QSystemTrayIcon::show(); + return; + } + mLauncher->show(); +} + +void QiliTray::connect(UserRole role, int room) +{ + // always store room to settings + mSettings->setRoom(room); + mRoom = room; + mRole = role; + + if (role == UserRole::Anonymous) { + mSettings->removeCookies(); + // clear cookies if any + mHttp->restoreCookies(); + } + else { + mSettings->setCookies(mHttp->storabelCookies()); + } + + + if (mConnection != nullptr) { + mConnection->disconnect(); + mConnection->deleteLater(); + } + mConnection = new QiliConnection(mHttp); + mConnection->setRoom(room); + mConnection->connect(); + QObject::connect(mConnection, &QiliConnection::authenticated, this, &QiliTray::onAuthenticated); + QObject::connect(mConnection, &QiliConnection::subtitleReceived, this, &QiliTray::onSubtitleReceived); + // QObject::connect(mConnection, &QiliConnection::watchersChanged, this, &QiliTray::onWatchersChanged); + QObject::connect(mConnection, &QiliConnection::errorOccured, this, &QiliTray::onConnectionError); + +} + +QiliSettings &QiliTray::settings() const +{ + return *mSettings; +} + +bool QiliTray::eventFilter(QObject *watched, QEvent *event) +{ + if (event->type() == QEvent::Close && watched == mLauncher) { + if (!this->started) { + qApp->exit(); + } + } + return QSystemTrayIcon::eventFilter(watched, event); +} + +void QiliTray::onLoggerTriggered() +{ + mSubtitleLogger->show(); + mSubtitleLogger->activateWindow(); + mSubtitleLogger->raise(); +} + +void QiliTray::onRememberChanged(bool remember) +{ + if (!remember) { + mSettings->removeCookies(); + } + mSettings->setKeepUser(remember); +} + +void QiliTray::onConnectTriggered() +{ + connect(mRole, mRoom); +} + +void QiliTray::onThanksTriggered() +{ + mThanksDialog->show(); + mThanksDialog->activateWindow(); +} + +void QiliTray::onAuthenticated() +{ + started = true; + show(); + showMessage(tr("Qili"), tr("Connected now, to exit use the system tray.")); + mLauncher->close(); + // onLoggerTriggered(); + emit connected(); +} + +void QiliTray::onSubtitleReceived(const QJsonObject &subtitle) +{ + QString cmd = subtitle / "cmd" >> JString; + + QString text; + + auto data = subtitle / "data" >> JObject; + if (cmd == "DANMU_MSG") { + QJsonArray data = subtitle / "info" >> JArray; + QString uname = data[2].toArray()[1] >> JString; + QString msg = data[1] >> JString; + auto extra = data[0].toArray()[15] >> JString >> JObject; + text = tr("%1 says %2").arg(Speakable::username(uname)).arg(msg); + QString now = QDateTime::currentDateTime().toString("MM/dd hh:mm:ss"); + mSubtitleLogger->subtitleReceived(QString("[%1] %2").arg(now).arg(text)); + } + else if (cmd == "INTERACT_WORD") { + QString uname = subtitle / "data" / "uinfo" / "base" / "name" >> JString; + text = tr("%1 enter room").arg(Speakable::username(uname)); + } + else if (cmd == "ENTRY_EFFECT") { + text = subtitle / "data" / "copy_writing" >> JString; + } + else if (cmd == "ONLINE_RANK_V2") { + auto data = subtitle / "data" >> JObject; + QJsonArray list; + if (data.contains("list")) { + list = data / "list" >> JArray; + } + else if (data.contains("online_list")) { + list = data / "online_list" >> JArray; + } + subtitle / "data" / "list" >> JArray; + QMap ranks; + for (const auto &item : std::as_const(list)) { + auto rank = item.toObject(); + QString uname = rank / "uname" >> JString; + int value = rank / "rank" >> JInt; + ranks[value] = uname; + } + qDebug() << "ranks = " << ranks; + } + else if (cmd == "PK_BATTLE_END") { + // auto begin = data / "init_info" / "best_uname" >> JString; + // auto end = data / "match_info" / "best_uname" >> JString; + // text = QString("most helpful warrior, %1 at begin, %2 at end").arg(begin).arg(end); + } + else if (cmd == "SEND_GIFT") { + auto action = data / "action" >> JString; + auto uname = data / "uname" >> JString; + auto gift = data / "giftName" >> JString; + auto batch = data / "super_gift_num" >> JInt; + auto num = data / "num" >> JInt; + text = tr("%1 %2 %3 %4 x %5").arg(Speakable::username(uname)).arg(action).arg(num).arg(gift).arg(batch); + } + else if (cmd == "LIKE_INFO_V3_CLICK") { + auto uname = data / "uname" >> JString; + auto like = data / "like_text" >> JString; + text = uname + like; + } + else if (cmd == "NOTICE_MSG") { + auto realRoom = subtitle / "real_roomid" >> JInt; + auto message = subtitle / "msg_self" >> JString; + if (realRoom == mRoom) { + text = message; + } + } + else if (cmd == "ONLINE_RANK_COUNT") { + //在线人数人数变更 + auto count = data / "count" >> JInt; + auto onlineCount = data / "online_count" >> JInt; + } + else if (cmd == "WATCHED_CHANGE") { + //几人看过本场直播 + auto count = data / "num" >> JInt; + //描述文本 + // text = data / "text_large" >> JString; + text = tr("%1 visitors until now").arg(count); + } + + if (!text.isEmpty()) { + mSpeaker->speak(text); + mSubtitleLogger->subtitleReceived(text); + } +} + +void QiliTray::onWatchersChanged(int watchers) +{ + mWatchers = watchers; + setToolTip(QString(tr("%1 Watcher(s)")).arg(mWatchers)); + mSubtitleLogger->subtitleReceived(QString(tr("[%1] Watcher(s): %2")).arg(QDateTime::currentDateTime().toString("MM/dd hh:mm:ss")).arg(watchers)); +} + +void QiliTray::onIconActived(ActivationReason reason) +{ + qDebug() << "Activated = " << reason; + switch(reason) { + case ActivationReason::Context: + // nothing need to do + break; + case ActivationReason::DoubleClick: + case ActivationReason::MiddleClick: + qDebug() << "Activated = MiddleClick"; + if (paused) { + mSpeaker->resume(); + paused = false; + showMessage(tr("Qili Speaker"), tr("Resumed")); + } else { + mSpeaker->pause(); + paused = true; + showMessage(tr("Qili Speaker"), tr("Paused")); + } + break; + default: + qDebug() << "Activated = Trigger"; + onLoggerTriggered(); + break; + } +} + +void QiliTray::onConnectionError(QiliConnection::Error error, const QString &errorString) +{ + showMessage(tr("Qili disconnected"), errorString); + if (mConnection != nullptr) { + mConnection->disconnect(); + mConnection->deleteLater(); + } +} + +void QiliTray::onVoiceApply(const QLocale &locale, const QVoice &voice, int volume, double pitch) +{ + mSpeaker->setLocale(locale); + mSpeaker->setVoice(voice); + mSpeaker->setVolume(volume); + mSpeaker->setPitch(pitch); + + mSettings->setSpeakerLocale(locale); + mSettings->setSpeakerVoice(voice.name()); + mSettings->setSpeakerVolume(volume); + mSettings->setSpeakerPitch(pitch); + qInfo() << "Speaker changed: locale = " << locale << " voice = " << voice.name(); + QMessageBox::information(nullptr, tr("Qili Speaker"), tr("Settings applied")); +} + +void QiliTray::onRestart() +{ + qApp->exit(QiliFlag::Restart); +} diff --git a/App/QiliTray.h b/App/QiliTray.h new file mode 100644 index 0000000..4543835 --- /dev/null +++ b/App/QiliTray.h @@ -0,0 +1,99 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILITRAY_H +#define QILITRAY_H + +#include "QiliAppGlobal.h" +#include "QiliHttp.h" +#include "QiliConnection.h" +#include "QiliLogin.h" +#include "QiliSubtitleLogger.h" +#include "QiliSpeaker.h" +#include "QiliSettings.h" +#include "QiliSettingsDialog.h" +#include "QiliThanksDialog.h" + +#include +#include +#include + + +class QILI_APP_EXPORT QiliTray : public QSystemTrayIcon +{ + Q_OBJECT +public: + explicit QiliTray(QObject *parent = nullptr); + ~QiliTray(); + +signals: + void connected(); + void errorOccured(QString message); + +public slots: + void show(); + void connect(UserRole role, int room); + +public: + QiliSettings &settings() const; + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +private slots: + void onRememberChanged(bool remember); + + void onIconActived(QSystemTrayIcon::ActivationReason reason); + + void onLoggerTriggered(); + void onConnectTriggered(); + void onThanksTriggered(); + + void onAuthenticated(); + void onSubtitleReceived(const QJsonObject &subtitle); + void onWatchersChanged(int watchers); + + void onConnectionError(QiliConnection::Error error, const QString &errorString); + void onVoiceApply(const QLocale &locale, const QVoice &voice, int volume, double pitch); + void onRestart(); + +private: + int mRoom; + UserRole mRole; + bool started{false}; + bool paused{false}; + int mWatchers{0}; + + QiliSettings *mSettings; + + QMenu *menu; + QAction *mConnectAction; + QAction *mLoggerAction; + QAction *mSettingsAction; + QAction *mThanksAction; + QAction *mRestartAction; + QAction *mExitAction; + + QiliLauncher *mLauncher; + QiliSettingsDialog *mSettingsDialog; + QiliSubtitleLogger *mSubtitleLogger; + QiliThanksDialog *mThanksDialog; + QiliConnection *mConnection{nullptr}; + QiliHttp *mHttp; + QiliSpeaker *mSpeaker; +}; + +#endif // QILITRAY_H diff --git a/App/Utility.cpp b/App/Utility.cpp new file mode 100644 index 0000000..22e99ce --- /dev/null +++ b/App/Utility.cpp @@ -0,0 +1,156 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "Utility.h" + +namespace Utility { + +QString Speakable::username(const QString &text) + { + QString speakable; + for (const auto &c : std::as_const(text)) { + if (c.isDigit()) { + int digit = c.digitValue(); + switch(digit) { + case 0: + speakable += Speakable::tr("0", "speakable-username"); + break; + case 1: + speakable += Speakable::tr("1", "speakable-username"); + break; + case 2: + speakable += Speakable::tr("2", "speakable-username"); + break; + case 3: + speakable += Speakable::tr("3", "speakable-username"); + break; + case 4: + speakable += Speakable::tr("4", "speakable-username"); + break; + case 5: + speakable += Speakable::tr("5", "speakable-username"); + break; + case 6: + speakable += Speakable::tr("6", "speakable-username"); + break; + case 7: + speakable += Speakable::tr("7", "speakable-username"); + break; + case 8: + speakable += Speakable::tr("8", "speakable-username"); + break; + case 9: + speakable += Speakable::tr("9", "speakable-username"); + break; + } + } else { + speakable += c; + } + } + return speakable; + } + +} + +namespace Utility::JSON { + + bool strongTyped() + { + if (!SpecializationEnabled) { + return StrongTyped; + } + return specialized.hasLocalData() ? specialized.localData() : StrongTyped; + }; + + QJsonObject operator>>(const QByteArray &bytes, JValueType) + { + return QJsonDocument::fromJson(bytes).object(); + } + + QByteArray operator>>(const QJsonObject &json, JValueType) + { + return QJsonDocument(json).toJson(QJsonDocument::Compact); + } + + QString operator>>(const QJsonObject &json, JValueType) + { + return QJsonDocument(json).toJson(QJsonDocument::Compact); + } + + QJsonValue JValueVisitor::value() const + { + const QJsonObject *current = &jobject; + for (auto iterator = jpaths.begin(), last = jpaths.end() - 1; iterator < jpaths.end(); iterator++) { + if (iterator == last) { + return current->value(*iterator); + } + QJsonObject value = current->value(*iterator).toObject(); + current = &value; + } + return QJsonValue::Undefined; + } + + template<> + QStringList operator>>(const QJsonValue &value, JValueType) + { + if (value.isNull() || value.isUndefined()) { + return QStringList(); + } + + if (!value.isArray()) { + if (strongTyped()) { + throw JTypeError(value.type()); + } + return QStringList(); + } + + QStringList list; + for (const auto &item : value.toArray()) { + if (!item.isString() && strongTyped()) { + throw JTypeError(item.type()); + } + list << item.toString(); + } + + return list; + } + + JValueVisitor& operator/(JValueVisitor&& visitor, const QString path) + { + visitor.jpaths << path; + return visitor; + } + JValueVisitor& operator/(JValueVisitor& visitor, const QString path) + { + visitor.jpaths << path; + return visitor; + } + + JValueVisitor operator/(const QJsonObject &object, const QString path) + { + return JValueVisitor(object, path); + } + + JValueVisitor operator/(const QJsonValueRef &object, const QString path) + { + return JValueVisitor(object.toObject(), path); + } + + JValueVisitor operator/(const QJsonValueRef &&object, const QString path) + { + return JValueVisitor(object.toObject(), path); + } +} diff --git a/App/Utility.h b/App/Utility.h new file mode 100644 index 0000000..b231cdd --- /dev/null +++ b/App/Utility.h @@ -0,0 +1,230 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef UTILITY_H +#define UTILITY_H + +#include "QiliAppGlobal.h" + +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace Utility { + +class Speakable { + Q_DECLARE_TR_FUNCTIONS(Text) + +public: + static QString username(const QString &text); +}; + +namespace JSON { + + static bool StrongTyped = false; + static bool SpecializationEnabled = false; + static QThreadStorage specialized; + + class JTypeError + { + public: + JTypeError(QJsonValue::Type type): actualType(type) {}; + const QJsonValue::Type actualType; + }; + + enum JSpecializationError { + SpecializationDisabled + }; + + // Marker for QJsonValue/QJsonObject <=> `Qt Native Type` converting + template + class JValueType {}; + + + static const JValueType JValue{}; + static const JValueType JObject{}; + static const JValueType JArray{}; + static const JValueType JBinary{}; + + + static const JValueType JBool{}; + static const JValueType> JBools{}; + + static const JValueType JInt{}; + static const JValueType> JInts{}; + + static const JValueType JDouble{}; + static const JValueType> JDoubles{}; + + static const JValueType JString{}; + static const JValueType JStrings{}; + + bool strongTyped(); + + QJsonObject operator>>(const QByteArray &bytes, JValueType); + QByteArray operator>>(const QJsonObject &json, JValueType); + + QString operator>>(const QJsonObject &json, JValueType); + + // templated converter for QJsonValue => `Qt Native Type` + template typename J = JValueType> + ToQt operator>>(const QJsonValue &value, const J); + + // QJsonValue => QJsonValue + template<> + inline QJsonValue operator>>(const QJsonValue &value, JValueType) + { + return value; + } + + // QJsonValue => QJsonArray + template<> + inline QJsonArray operator>>(const QJsonValue &value, JValueType) + { + return value.toArray(); + } + + + // QJsonValue => QJsonObject + template<> + inline QJsonObject operator>>(const QJsonValue &value, JValueType) + { + if (value.isNull()) { + return value.toObject(); + } + if (value.isUndefined()) { + return value.toObject(); + } + + if (!value.isObject() && strongTyped()) { + throw JTypeError(value.type()); + } + + return value.toObject(); + } + + // QJsonValue => QStringList + template<> + QStringList operator>>(const QJsonValue &value, JValueType); + + template<> + inline bool operator>>(const QJsonValue &value, JValueType) + { + if (!value.isBool() && strongTyped()) { + throw JTypeError(value.type()); + } + return value.toBool(); + } + + // QJsonValue => int + template<> + inline int operator>>(const QJsonValue &value, JValueType) + { + if (!value.isDouble() && strongTyped()) { + throw JTypeError(value.type()); + } + return value.toInt(); + } + + // QJsonValue => double + template<> + inline double operator>>(const QJsonValue &value, JValueType) + { + if (!value.isDouble() && strongTyped()) { + throw JTypeError(value.type()); + } + return value.toDouble(); + } + + // QJsonValue => QString + template<> + inline QString operator>>(const QJsonValue &value, JValueType) + { + if (!value.isString() && strongTyped()) { + throw JTypeError(value.type()); + } + return value.toString(); + } + + // A wrapper for json tree traversing + class JValueVisitor + { + friend JValueVisitor& operator/(JValueVisitor&& visitor, const QString path); + friend JValueVisitor& operator/(JValueVisitor& visitor, const QString path); + + public: + JValueVisitor(const QJsonObject &object, const QString &path): jobject(object), jpaths(path) {}; + + QJsonValue value() const; + + private: + QStringList jpaths; + const QJsonObject &jobject; + }; + + // Json Tree Visitor + JValueVisitor operator/(const QJsonObject &object, const QString path); + JValueVisitor& operator/(JValueVisitor &&visitor, const QString path); + JValueVisitor& operator/(JValueVisitor &visitor, const QString path); + JValueVisitor operator/(const QJsonValueRef &object, const QString path); + + // Json Tree Visitor => Qt Native Type + template typename J = JValueType> + ToQt operator>>(const JValueVisitor &visitor, const J type) + { + return visitor.value() >> type; + } + + // Fo spicialized code block which not using global type checking settings + template + T withoutChecking(T (*getter)()) + { + if (!StrongTyped) { + return getter(); + } + if (!SpecializationEnabled) { + throw JSpecializationError::SpecializationDisabled; + } + specialized.setLocalData(false); + T &value = getter(); + specialized.setLocalData(StrongTyped); + return value; + } + + template + T withChecking(T (*getter)()) + { + if (StrongTyped) { + return getter(); + } + if (!SpecializationEnabled) { + throw JSpecializationError::SpecializationDisabled; + } + specialized.setLocalData(true); + T &value = getter(); + specialized.setLocalData(StrongTyped); + return value; + } +}; // JSON +}; // Utility + +#endif // UTILITY_H diff --git a/App/images/alipay.png b/App/images/alipay.png new file mode 100644 index 0000000000000000000000000000000000000000..236b3c9d259aa504237a09079bf2718ff13dedd0 GIT binary patch literal 89740 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

s8UfHW=@@b*ZXY>87E^`q7tX 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 V&XCCD)B9QKD%V@;{YZG`Ro( literal 0 HcmV?d00001 diff --git a/App/images/qili.ico b/App/images/qili.ico new file mode 100644 index 0000000000000000000000000000000000000000..ac1dfa915e460a633ea61c28b7adb0009bd81c83 GIT binary patch literal 19374 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 WUO$x>oK_D4KP@%=>lG>%VgCdAxn(o} literal 0 HcmV?d00001 diff --git a/App/main.cpp b/App/main.cpp new file mode 100644 index 0000000..8cffe98 --- /dev/null +++ b/App/main.cpp @@ -0,0 +1,93 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliGlobal.h" +#include "QiliLogger.h" + +#include +#include +#include +#include + +using QiliAppType = int (*)(QApplication &app); + +QStringList libsForQili(); + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + QiliLogger::install(); + qDebug() << "Starting..."; + + QDir dir = app.applicationDirPath(); + dir.cdUp(); + + const auto libs = libsForQili(); + for (const auto &lib : std::as_const(libs)) { + auto path = dir.absoluteFilePath(lib); + if (QFile(path).exists()) { + QCoreApplication::addLibraryPath(QString(path)); + qDebug() << "Added to Library Path: " << path; + } + } + const auto libraryPaths = QCoreApplication::libraryPaths(); + qDebug() << "Final libraryPaths = " << libraryPaths; + + QiliAppType QiliApp = nullptr; + const auto modules = { "QRCodeGen", "QiliWidgets", "QiliApp" }; + for (const auto &module : modules) { + for (const auto &path : std::as_const(libraryPaths)) { + QLibrary lib(path + QDir::separator() + module); + if (lib.load()) { + qDebug() << "Loaded Module = " << module << " => " << lib.fileName(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + if (std::strcmp(module, "QiliApp") == 0) { +#else + if (strcmp(module, "QiliApp") == 0) { +#endif + QiliApp = (QiliAppType) lib.resolve("QiliApp"); + } + break; + } else { + qDebug() << "Can't load : " << module << " => " << lib.errorString(); + } + } + } + if (QiliApp == nullptr) { + qCritical() << "QiliApp not found in: " << libraryPaths; + return -1; + } + + qDebug() << "Started"; + + int code = 0; + do { + code = QiliApp(app); + } while (code == Qili::Restart); + + return code; +} + +QStringList libsForQili() +{ + if (Qili::Released) { + return { "lib64", "lib", "plugins" }; + } + else { + // for development layout + return {"Thirdparty", "Widgets", "App"}; + } +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c6e25fd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,187 @@ +# Copyright (C) 2024 Sauntor + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +cmake_minimum_required(VERSION 3.5) + +project( + Qili VERSION 1.0.0 LANGUAGES CXX + HOMEPAGE_URL https://gitee.com/sauntor/Qili + DESCRIPTION "A subtitle spearker for live broadcasting at bilibili.com" +) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if (CMAKE_BUILD_TYPE MATCHES "^[Rr]el|[Rr]el$") + set(QILI_RELEASE_BUILD ON) +endif() + +# OS Type for Qili +if (WIN32) + set(QILI_ON_WIN32 ON) +endif() +if (APPLE) + set(QILI_ON_APPLE ON) +endif() +if (LINUX) + set(QILI_ON_LINUX ON) + cmake_host_system_information(RESULT DISTRO QUERY DISTRIB_INFO) +endif() +if (UNIX) + set(QILI_ON_UNIX ON) +endif() + +option(DEV_MODE "Enable developing mode?" OFF) +option(USE_QT5 "Force to build Qili using Qt5?" OFF) +option(USE_IFW "Use Qt IFW for packaging?" OFF) +option(USE_CPACK "Use CPack for packaging?" OFF) + +if (USE_CPACK) + set(QILI_PACK_TYPE "CPACK") + set(QILI_INSTALL_PREFIX "/usr") +endif() +if (USE_IFW) + if (USE_CPACK) + message(FATAL_ERROR "Please DO NOT use IFW and CPack together!") + endif() + set(QILI_PACK_TYPE "IFW") + set(QILI_INSTALL_PREFIX "/opt") +endif() +if (DEFINED QILI_PACK_TYPE) + message(WARNING "Enter ${QILI_PACK_TYPE} packaging mode") + if (NOT QILI_RELEASE_BUILD) + message(WARNING "Please use ${QILI_PACK_TYPE} packaging mode with \"Release\" build type") + endif() +endif() +message(STATUS "CMAKE_INSTALL_PREFIX = ${CMAKE_INSTALL_PREFIX}") +if (NOT QILI_INSTALL_PREFIX) + set(QILI_INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX}) +endif() + +configure_file(Config.h.in ${CMAKE_BINARY_DIR}/Config.h @ONLY) + +set(QILI_QT_DEPS + Core + Gui + Network + WebSockets + TextToSpeech + Widgets + LinguistTools +) +if (DEV_MODE OR NOT QILI_RELEASE_BUILD) + list(APPEND QILI_QT_DEPS UiPlugin) +endif() + +if (USE_QT5) + message(CHECK_START "Detecting Qt5") + find_package(QT NAMES Qt5 5.15 REQUIRED COMPONENTS ${QILI_QT_DEPS}) + find_package(Qt5 REQUIRED COMPONENTS ${QILI_QT_DEPS}) + if (NOT QT_FOUND) + message(CHECK_FAIL "Not Found") + else() + message(CHECK_PASS "Using Qt ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}") + endif() +else() + message(CHECK_START "Detecting Qt5/Qt6") + find_package(QT NAMES Qt6 REQUIRED COMPONENTS ${QILI_QT_DEPS}) + if (NOT QT_FOUND) + find_package(QT NAMES Qt5 REQUIRED COMPONENTS ${QILI_QT_DEPS}) + endif() + if (NOT QT_FOUND) + message(CHECK_FAIL "Not Found") + else() + find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS ${QILI_QT_DEPS}) + message(CHECK_PASS "Using Qt ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}") + endif() +endif() + +if (QT_VERSION_MAJOR EQUAL 6) + include("${Qt${QT_VERSION_MAJOR}_DIR}/FindWrapBrotli.cmake") +else() + include(FindWrapBrotli.cmake) +endif() + +function(qili_project_setup) + if (${QT_VERSION_MAJOR} GREATER 5) + qt_standard_project_setup() + endif() +endfunction() + +function(qili_add_executable name) + if (QT_VERSION_MAJOR GREATER 5) + qt_add_executable(${name} WIN32 MACOSX_BUNDLE MANUAL_FINALIZATION) + else() + add_executable(${name} WIN32 MACOSX_BUNDLE) + endif() +endfunction() + + +function(qili_add_library name) + if (QT_VERSION_MAJOR GREATER 5) + qt_add_library(${name} SHARED MANUAL_FINALIZATION) + else() + add_library(${name} SHARED) + endif() +endfunction() + +function(qili_add_plugin name) + if (QT_VERSION_MAJOR GREATER 5) + qt_add_plugin(${name} SHARED MANUAL_FINALIZATION) + else() + add_library(${name} SHARED) + endif() +endfunction() + +function(qili_finalize_target name) + if (${QT_VERSION_MAJOR} GREATER 5) + qt_finalize_target(${name}) + endif() +endfunction() +function(qili_finalize_executable name) + if (${QT_VERSION_MAJOR} GREATER 5) + qt_finalize_executable(${name}) + endif() +endfunction() +qili_project_setup() + +include(GNUInstallDirs) + + +add_subdirectory(Thirdparty) +add_subdirectory(Widgets) +if (DEV_MODE OR NOT QILI_RELEASE_BUILD) + add_subdirectory(Designer) +endif() +add_subdirectory(App) + +include("${CMAKE_SOURCE_DIR}/QiliInstall.cmake") + + +if (USE_CPACK) + configure_file ("${CMAKE_SOURCE_DIR}/QiliCPack.cmake" + "${CMAKE_BINARY_DIR}/QiliCPack.cmake" + @ONLY) + set(CPACK_PROPERTIES_FILE "${CMAKE_BINARY_DIR}/QiliCPack.cmake") + include(CPack) +endif() + +if (USE_IFW) + include("${CMAKE_SOURCE_DIR}/QiliIFW.cmake") +endif() diff --git a/Config.h.in b/Config.h.in new file mode 100644 index 0000000..d7fd3e5 --- /dev/null +++ b/Config.h.in @@ -0,0 +1,27 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef CONFIG_H +#define CONFIG_H + +#define QILI_VERSION "@Qili_VERSION@" +#cmakedefine01 QILI_RELEASE_BUILD +#cmakedefine01 QILI_ON_WIN32 +#cmakedefine01 QILI_ON_LINUX +#cmakedefine01 QILI_ON_UNIX +#cmakedefine01 QILI_ON_APPLE + +#endif // CONFIG_H diff --git a/Designer/CMakeLists.txt b/Designer/CMakeLists.txt new file mode 100644 index 0000000..9aacf63 --- /dev/null +++ b/Designer/CMakeLists.txt @@ -0,0 +1,51 @@ +# Copyright (C) 2024 Sauntor + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +############################################ +# Qili widgets plugin for Qt 6 Designer # +############################################ + +set(DESIGNER_TS_FILES QiliDesigner_zh_CN.ts) + +qili_add_plugin(QiliDesigner) +target_compile_definitions(QiliDesigner PRIVATE QILI_DESIGNER_LIBRARY) + +target_sources(QiliDesigner PRIVATE + DesignerGlobal.h + QiliDesigner.h QiliDesigner.cpp + QiliTitleBarDesigner.h QiliTitleBarDesigner.cpp + QiliTextFieldDesigner.h QiliTextFieldDesigner.cpp + ${DESIGNER_RC_FILES} +) +target_link_libraries(QiliDesigner PRIVATE + Qt::Core + Qt::Gui + Qt::UiPlugin + Qt::Widgets + $ +) +add_dependencies(QiliDesigner QiliWidgets) +target_include_directories(QiliDesigner PUBLIC ${PROJECT_SOURCE_DIR}/Widgets) + +set(QDESIGNER_PLUGINS_DIR plugins/designer) + +install(TARGETS QiliDesigner + BUNDLE DESTINATION ${QDESIGNER_PLUGINS_DIR} + LIBRARY DESTINATION ${QDESIGNER_PLUGINS_DIR} + RUNTIME DESTINATION ${QDESIGNER_PLUGINS_DIR} + COMPONENT Development +) +qili_finalize_target(QiliDesigner) diff --git a/Designer/DesignerGlobal.h b/Designer/DesignerGlobal.h new file mode 100644 index 0000000..be47e84 --- /dev/null +++ b/Designer/DesignerGlobal.h @@ -0,0 +1,28 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef DESIGNERGLOBAL_H +#define DESIGNERGLOBAL_H + +#include + +#if defined(QILI_DESIGNER_LIBRARY) +#define QILI_DESIGNER_EXPORT Q_DECL_EXPORT +#else +#define QILI_DESIGNER_EXPORT Q_DECL_IMPORT +#endif + +#endif // DESIGNERGLOBAL_H diff --git a/Designer/QiliDesigner.cpp b/Designer/QiliDesigner.cpp new file mode 100644 index 0000000..e1876a1 --- /dev/null +++ b/Designer/QiliDesigner.cpp @@ -0,0 +1,32 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliDesigner.h" + +#include "QiliTextFieldDesigner.h" +#include "QiliTitleBarDesigner.h" + +QiliDesigner::QiliDesigner(QObject *parent) + : QObject{parent} +{ + designers += new QiliTextFieldDesigner(this); + designers += new QiliTitleBarDesigner(this); +} + +QList QiliDesigner::customWidgets() const +{ + return designers; +} diff --git a/Designer/QiliDesigner.h b/Designer/QiliDesigner.h new file mode 100644 index 0000000..d4af522 --- /dev/null +++ b/Designer/QiliDesigner.h @@ -0,0 +1,39 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILIDESIGNER_H +#define QILIDESIGNER_H + +#include "DesignerGlobal.h" + +#include +#include + +class QILI_DESIGNER_EXPORT QiliDesigner : public QObject, QDesignerCustomWidgetCollectionInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QDesignerCustomWidgetCollectionInterface") + Q_INTERFACES(QDesignerCustomWidgetCollectionInterface) +public: + explicit QiliDesigner(QObject *parent = nullptr); + + QList customWidgets() const override; + +private: + QList designers; +}; + +#endif // QILIDESIGNER_H diff --git a/Designer/QiliTextFieldDesigner.cpp b/Designer/QiliTextFieldDesigner.cpp new file mode 100644 index 0000000..80b6fa5 --- /dev/null +++ b/Designer/QiliTextFieldDesigner.cpp @@ -0,0 +1,116 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliTextFieldDesigner.h" + +#include "QiliTextField.h" + +#include +#include +#include + +// using namespace Qt::StringLiterals; + +QiliTextFieldDesigner::QiliTextFieldDesigner(QObject *parent) + : QObject{parent} +{} + +bool QiliTextFieldDesigner::isContainer() const +{ + return false; +} + +bool QiliTextFieldDesigner::isInitialized() const +{ + return initialized; +} + +QIcon QiliTextFieldDesigner::icon() const +{ + return QIcon(":/images/qili.png"); +} + +QString QiliTextFieldDesigner::domXml() const +{ + return QString::fromUtf16(uR"( + + +)" +uR"( + + + 0 + 0 + 200 + 31 + + + + text-field + +") +uR"( + + +)"); +} + +QString QiliTextFieldDesigner::group() const +{ + return QString::fromUtf16(u"Display Widgets"); +} + +QString QiliTextFieldDesigner::includeFile() const +{ + return QString::fromUtf16(u"QiliTextField.h"); +} + +QString QiliTextFieldDesigner::name() const +{ + return QString::fromUtf16(u"QiliTextField"); +} + +QString QiliTextFieldDesigner::toolTip() const +{ + return QString::fromUtf16(u"Qili Text Field"); +} + +QString QiliTextFieldDesigner::whatsThis() const +{ + return QString::fromUtf16(u"Text Field for Qili"); +} + +QWidget *QiliTextFieldDesigner::createWidget(QWidget *parent) +{ + QiliTextField *widget = new QiliTextField(parent); + if (!styleSheet.isEmpty()) { + widget->setStyleSheet(styleSheet); + } + return widget; +} + +void QiliTextFieldDesigner::initialize(QDesignerFormEditorInterface *core) +{ + if (initialized) { + return; + } + QFile file(":/themes/light.css"); + if (file.open(QFile::ReadOnly)) { + styleSheet = file.readAll(); + file.close(); + } + initialized = true; +} diff --git a/Designer/QiliTextFieldDesigner.h b/Designer/QiliTextFieldDesigner.h new file mode 100644 index 0000000..c7b4ca3 --- /dev/null +++ b/Designer/QiliTextFieldDesigner.h @@ -0,0 +1,49 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILITEXTFIELDDESIGNER_H +#define QILITEXTFIELDDESIGNER_H + +#include "DesignerGlobal.h" + +#include + +class QILI_DESIGNER_EXPORT QiliTextFieldDesigner : public QObject, public QDesignerCustomWidgetInterface +{ + Q_OBJECT + // Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QDesignerCustomWidgetInterface") + Q_INTERFACES(QDesignerCustomWidgetInterface) + +public: + explicit QiliTextFieldDesigner(QObject *parent = nullptr); + bool isContainer() const override; + bool isInitialized() const override; + QIcon icon() const override; + QString domXml() const override; + QString group() const override; + QString includeFile() const override; + QString name() const override; + QString toolTip() const override; + QString whatsThis() const override; + QWidget *createWidget(QWidget *parent) override; + void initialize(QDesignerFormEditorInterface *core) override; + +private: + bool initialized = false; + QString styleSheet; +}; + +#endif // QILITEXTFIELDDESIGNER_H diff --git a/Designer/QiliTitleBarDesigner.cpp b/Designer/QiliTitleBarDesigner.cpp new file mode 100644 index 0000000..5b1a063 --- /dev/null +++ b/Designer/QiliTitleBarDesigner.cpp @@ -0,0 +1,120 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliTitleBarDesigner.h" + +#include "QiliTitleBar.h" + +#include +#include +#include + +// using namespace Qt::StringLiterals; + +QiliTitleBarDesigner::QiliTitleBarDesigner(QObject *parent) + : QObject{parent} +{ + +} + +bool QiliTitleBarDesigner::isContainer() const +{ + return false; +} + +bool QiliTitleBarDesigner::isInitialized() const +{ + return initialized; +} + +QIcon QiliTitleBarDesigner::icon() const +{ + return QIcon(":/images/qili.png"); +} + +QString QiliTitleBarDesigner::domXml() const +{ + + + return QString::fromUtf16(uR"( + + +)" +uR"( + + + 0 + 0 + 400 + 26 + + + + titlebar + +") +uR"( + + +)"); +} + +QString QiliTitleBarDesigner::group() const +{ + return QString::fromUtf16(u"Display Widgets"); +} + +QString QiliTitleBarDesigner::includeFile() const +{ + return QString::fromUtf16(u"QiliTitleBar.h"); +} + +QString QiliTitleBarDesigner::name() const +{ + return QString::fromUtf16(u"QiliTitleBar"); +} + +QString QiliTitleBarDesigner::toolTip() const +{ + return QString::fromUtf16(u"Qili Title Bar"); +} + +QString QiliTitleBarDesigner::whatsThis() const +{ + return QString::fromUtf16(u"Title Bar for Qili"); +} + +QWidget *QiliTitleBarDesigner::createWidget(QWidget *parent) +{ + auto *widget = new QiliTitleBar(parent); + if (!styleSheet.isEmpty()) { + widget->setStyleSheet(styleSheet); + } + return widget; +} + +void QiliTitleBarDesigner::initialize(QDesignerFormEditorInterface *core) +{ + if (initialized) { + return; + } + QFile file(":/themes/light.css"); + if (file.open(QFile::ReadOnly)) { + styleSheet = file.readAll(); + file.close(); + } + initialized = true; +} diff --git a/Designer/QiliTitleBarDesigner.h b/Designer/QiliTitleBarDesigner.h new file mode 100644 index 0000000..89661c0 --- /dev/null +++ b/Designer/QiliTitleBarDesigner.h @@ -0,0 +1,51 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILITITLEBARDESIGNER_H +#define QILITITLEBARDESIGNER_H + +#include "DesignerGlobal.h" + +#include +#include + +class QILI_DESIGNER_EXPORT QiliTitleBarDesigner : public QObject, public QDesignerCustomWidgetInterface +{ + Q_OBJECT + // Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QDesignerCustomWidgetInterface") + Q_INTERFACES(QDesignerCustomWidgetInterface) + +public: + explicit QiliTitleBarDesigner(QObject *parent = nullptr); + + bool isContainer() const override; + bool isInitialized() const override; + QIcon icon() const override; + QString domXml() const override; + QString group() const override; + QString includeFile() const override; + QString name() const override; + QString toolTip() const override; + QString whatsThis() const override; + QWidget *createWidget(QWidget *parent) override; + void initialize(QDesignerFormEditorInterface *core) override; + +private: + bool initialized = false; + QString styleSheet; +}; + +#endif // QILITITLEBARDESIGNER_H diff --git a/FindWrapBrotli.cmake b/FindWrapBrotli.cmake new file mode 100644 index 0000000..e2d7b56 --- /dev/null +++ b/FindWrapBrotli.cmake @@ -0,0 +1,100 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +if(TARGET WrapBrotli::WrapBrotliDec) + set(WrapBrotli_FOUND ON) + return() +endif() + +# From VCPKG +find_package(unofficial-brotli CONFIG QUIET) +if (unofficial-brotli_FOUND) + add_library(WrapBrotli::WrapBrotliDec INTERFACE IMPORTED) + target_link_libraries(WrapBrotli::WrapBrotliDec INTERFACE unofficial::brotli::brotlidec) + + add_library(WrapBrotli::WrapBrotliEnc INTERFACE IMPORTED) + target_link_libraries(WrapBrotli::WrapBrotliEnc INTERFACE unofficial::brotli::brotlienc) + + add_library(WrapBrotli::WrapBrotliCommon INTERFACE IMPORTED) + target_link_libraries(WrapBrotli::WrapBrotliCommon INTERFACE unofficial::brotli::brotlicommon) + + set(WrapBrotli_FOUND ON) +else() + find_package(PkgConfig QUIET) + if (PKG_CONFIG_FOUND) + pkg_check_modules(libbrotlidec QUIET IMPORTED_TARGET "libbrotlidec") + if (libbrotlidec_FOUND) + add_library(WrapBrotli::WrapBrotliDec INTERFACE IMPORTED) + target_link_libraries(WrapBrotli::WrapBrotliDec INTERFACE PkgConfig::libbrotlidec) + set(WrapBrotli_FOUND ON) + endif() + + pkg_check_modules(libbrotlienc QUIET IMPORTED_TARGET "libbrotlienc") + if (libbrotlienc_FOUND) + add_library(WrapBrotli::WrapBrotliEnc INTERFACE IMPORTED) + target_link_libraries(WrapBrotli::WrapBrotliEnc INTERFACE PkgConfig::libbrotlienc) + set(WrapBrotli_FOUND ON) + endif() + + pkg_check_modules(libbrotlicommon QUIET IMPORTED_TARGET "libbrotlicommon") + if (libbrotlicommon_FOUND) + add_library(WrapBrotli::WrapBrotliCommon INTERFACE IMPORTED) + target_link_libraries(WrapBrotli::WrapBrotliCommon INTERFACE PkgConfig::libbrotlicommon) + set(WrapBrotli_FOUND ON) + endif() + else() + find_path(BROTLI_INCLUDE_DIR NAMES "brotli/decode.h") + + foreach(lib_name BrotliDec BrotliEnc BrotliCommon) + string(TOLOWER ${lib_name} lower_lib_name) + + find_library(${lib_name}_LIBRARY_RELEASE + NAMES ${lower_lib_name} ${lower_lib_name}-static) + + find_library(${lib_name}_LIBRARY_DEBUG + NAMES ${lower_lib_name}d ${lower_lib_name}-staticd + ${lower_lib_name} ${lower_lib_name}-static) + + include(SelectLibraryConfigurations) + select_library_configurations(${lib_name}) + + if (BROTLI_INCLUDE_DIR AND ${lib_name}_LIBRARY) + set(${lib_name}_FOUND TRUE) + endif() + + if (${lib_name}_FOUND AND NOT TARGET WrapBrotli::Wrap${lib_name}) + add_library(WrapBrotli::Wrap${lib_name} UNKNOWN IMPORTED) + set_target_properties(WrapBrotli::Wrap${lib_name} PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${BROTLI_INCLUDE_DIR}" + IMPORTED_LOCATION "${${lib_name}_LIBRARY}") + + if(${lib_name}_LIBRARY_RELEASE) + foreach(config_name RELEASE RELWITHDEBINFO MINSIZEREL) + set_property(TARGET WrapBrotli::Wrap${lib_name} APPEND PROPERTY + IMPORTED_CONFIGURATIONS ${config_name}) + set_target_properties(WrapBrotli::Wrap${lib_name} PROPERTIES + IMPORTED_LOCATION_${config_name} "${${lib_name}_LIBRARY_RELEASE}") + endforeach() + endif() + + if(${lib_name}_LIBRARY_DEBUG) + set_property(TARGET WrapBrotli::Wrap${lib_name} APPEND PROPERTY + IMPORTED_CONFIGURATIONS DEBUG) + set_target_properties(WrapBrotli::Wrap${lib_name} PROPERTIES + IMPORTED_LOCATION_DEBUG "${${lib_name}_LIBRARY_DEBUG}") + endif() + endif() + endforeach() + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(WrapBrotli REQUIRED_VARS + BrotliDec_FOUND BrotliEnc_FOUND BrotliCommon_FOUND) + + if (WrapBrotli_FOUND) + set_property(TARGET WrapBrotli::WrapBrotliDec APPEND PROPERTY + INTERFACE_LINK_LIBRARIES WrapBrotli::WrapBrotliCommon) + set_property(TARGET WrapBrotli::WrapBrotliEnc APPEND PROPERTY + INTERFACE_LINK_LIBRARIES WrapBrotli::WrapBrotliCommon) + endif() + endif() +endif() diff --git a/IFW/config/controller.qs b/IFW/config/controller.qs new file mode 100644 index 0000000..1fa94b1 --- /dev/null +++ b/IFW/config/controller.qs @@ -0,0 +1,8 @@ +function Controller() +{ + console.log(`Controller @ ${new Date}`) + installer.componentAdded.connect(Controller.prototype.onCompnentAdded.bind(this)); +} +Controller.prototype.onCompnentAdded = function(added) { + console.log(`CompnentAdded: ${added.displayName}`) +} diff --git a/IFW/config/general.xml b/IFW/config/general.xml new file mode 100644 index 0000000..3c40245 --- /dev/null +++ b/IFW/config/general.xml @@ -0,0 +1,26 @@ + + + Qili + 1.0.0 + Qili + Sauntor OSS + Qili + @ApplicationsDir@/QiliApp + ../data/share/pixmaps/qili.png + ../data/share/pixmaps/qili.png + 560 + 390 + controller.qs + + ifw_en + ifw_zh_CN + ifw_zh_TW + + qt_en + qt_zh_CN + qt_zh_TW + + zh_CN.qm + + + diff --git a/IFW/packages/me.sauntor.qili/meta/desktop.qs b/IFW/packages/me.sauntor.qili/meta/desktop.qs new file mode 100644 index 0000000..a0abbfa --- /dev/null +++ b/IFW/packages/me.sauntor.qili/meta/desktop.qs @@ -0,0 +1,33 @@ + function Component() + { + // default constructor + } + + Component.prototype.createOperations = function() + { + component.createOperations(); + console.log("systemInfo", systemInfo.kernelType, systemInfo.productType, systemInfo); + if (systemInfo.kernelType === "winnt") { + component.addOperation("CreateShortcut", "@TargetDir@\\bin\\Qili.exe", "@StartMenuDir@/Qili.lnk", + "workingDirectory=@TargetDir@", "description=@ProductName@"); + } + else if (systemInfo.kernelType === "linux") { + component.addOperation("CreateDesktopEntry", "me.sauntor.qili.desktop", + `Name=Qili +Name[zh_CN]=Qili弹幕姬 +Name[zh_TW]=Qili彈幕姬 +GenericName=@ProductName@ +GenericName[zh_CN]=B站直播间弹幕朗读工具 +GenericName[zh_TW]=B站直播間彈幕朗讀者 +Categories=Network;AudioVideo; +Encoding=UTF-8 +Exec=@TargetDir@/bin/Qili +X-KDE-StartupNotify=true +StartupNotify=true +Terminal=false +Type=Application +Icon=@TargetDir@/share/pixmaps/qili.png +` + ); + } + } diff --git a/IFW/packages/me.sauntor.qili/meta/package.xml b/IFW/packages/me.sauntor.qili/meta/package.xml new file mode 100644 index 0000000..5513778 --- /dev/null +++ b/IFW/packages/me.sauntor.qili/meta/package.xml @@ -0,0 +1,12 @@ + + + Qili + Qili弹幕姬 + 1.0.1 + 2024-01-10 + + + + true + + diff --git a/IFW/packages/me.sauntor.qili/meta/zh_CN.ts b/IFW/packages/me.sauntor.qili/meta/zh_CN.ts new file mode 100644 index 0000000..ab31f6e --- /dev/null +++ b/IFW/packages/me.sauntor.qili/meta/zh_CN.ts @@ -0,0 +1,11 @@ + + + + + Text + + Qili + Qili弹幕姬 + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c3f5b52 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified +it, and giving a relevant date. + +b) The work must carry prominent notices stating that it is +released under this License and any conditions added under section +7. This requirement modifies the requirement in section 4 to +"keep intact all notices". + +c) You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +a) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either (1) a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. + +d) Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or +authors of the material; or + +e) Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + +Copyright (C) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) +This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +This is free software, and you are welcome to redistribute it +under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Linux/deb/postinst b/Linux/deb/postinst new file mode 100644 index 0000000..621fcd0 --- /dev/null +++ b/Linux/deb/postinst @@ -0,0 +1 @@ +#cp -fv /opt/me.sauntor.qili/share/applications/me.sauntor.qili.desktop /usr/share/applications/me.sauntor.qili.desktop diff --git a/Linux/deb/postrm b/Linux/deb/postrm new file mode 100644 index 0000000..160141b --- /dev/null +++ b/Linux/deb/postrm @@ -0,0 +1 @@ +#rm -fv /usr/applications/me.sauntor.qili.desktop diff --git a/Linux/me.sauntor.qili.desktop.in b/Linux/me.sauntor.qili.desktop.in new file mode 100644 index 0000000..444352d --- /dev/null +++ b/Linux/me.sauntor.qili.desktop.in @@ -0,0 +1,15 @@ +[Desktop Entry] +Type=Application +Name=Qili +Name[zh_CN]=Qili弹幕姬 +Name[zh_TW]=Qili彈幕姬 +Categories=Network;AudioVideo; +Encoding=UTF-8 +Exec=@QILI_INSTALL_PREFIX@/bin/Qili +GenericName=Bilibili Live Speaker +GenericName[zh_CN]=B站直播间弹幕朗读工具 +GenericName[zh_TW]=B站直播間彈幕朗讀者 +X-KDE-StartupNotify=true +Terminal=false +Type=Application +Icon=@QILI_INSTALL_PREFIX@/@CMAKE_INSTALL_DATADIR@/pixmaps/qili.png diff --git a/Linux/rpm/postinst b/Linux/rpm/postinst new file mode 100644 index 0000000..e27a34d --- /dev/null +++ b/Linux/rpm/postinst @@ -0,0 +1 @@ +#cp -fv /opt/%{name}/share/applications/%{name}.desktop %{_datadir}/applications/%{name}.desktop diff --git a/Linux/rpm/postrm b/Linux/rpm/postrm new file mode 100644 index 0000000..5aecd74 --- /dev/null +++ b/Linux/rpm/postrm @@ -0,0 +1 @@ +#rm -fv %{_datadir}/applications/%{name}.desktop diff --git a/QiliCPack.cmake b/QiliCPack.cmake new file mode 100644 index 0000000..f9650a9 --- /dev/null +++ b/QiliCPack.cmake @@ -0,0 +1,51 @@ +# Copyright (C) 2024 Sauntor + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +cmake_host_system_information(RESULT DISTRO QUERY DISTRIB_INFO) + +set(CPACK_PACKAGE_NAME me.sauntor.qili) +set(CPACK_PACKAGE_VENDOR "Sauntor OSS") +set(CPACK_PACKAGE_CONTACT "Sauntor ") +set(CPACK_PACKAGE_SUMMARY "@Qili_DESCRIPTION@") +set(CPACK_PACKAGE_DESCRIPTION "@Qili_DESCRIPTION@") +set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${DISTRO_ID}.${DISTRO_VERSION_ID}_${CMAKE_SYSTEM_PROCESSOR}") +set(CPACK_RESOURCE_FILE_LICENSE "@CMAKE_SOURCE_DIR@/LICENSE") +set(CPACK_RESOURCE_FILE_README "@CMAKE_SOURCE_DIR@/README.md") + +set(QILI_INSTALL_PREFIX "@QILI_INSTALL_PREFIX@") +set(CPACK_PACKAGING_INSTALL_PREFIX "@QILI_INSTALL_PREFIX@") + +# RPM +set(CPACK_RPM_CHANGELOG_FILE "@CMAKE_SOURCE_DIR@/changelog") +set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE "@CMAKE_SOURCE_DIR@/Linux/rpm/postinst") +set(CPACK_RPM_POST_UNINSTALL_SCRIPT_FILE "@CMAKE_SOURCE_DIR@/Linux/rpm/postrm") +set(CPACK_RPM_PACKAGE_DESCRIPTION "${CPACK_PACKAGE_SUMMARY}") + +set(CPACK_RPM_PACKAGE_GROUP "Applications/Internet") +set(CPACK_RPM_CHANGELOG_FILE "@CMAKE_SOURCE_DIR@/changelog") + +# DEB for ubuntu/kylin 22.04 +set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA + "@CMAKE_SOURCE_DIR@/Linux/deb/postinst;@CMAKE_SOURCE_DIR@/Linux/deb/postrm" +) +set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5widgets5") +string(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS ", libqt5network5") +string(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS ", libqt5websockets5") +string(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS ", libqt5websockets5") +string(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS ", libqt5texttospeech5") +set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "speech-dispatcher") +string(APPEND CPACK_DEBIAN_PACKAGE_RECOMMENDS ", speech-dispatcher-espeak-ng") +string(APPEND CPACK_DEBIAN_PACKAGE_RECOMMENDS ", espeak-ng") +string(APPEND CPACK_DEBIAN_PACKAGE_RECOMMENDS ", mbrola") diff --git a/QiliIFW.cmake b/QiliIFW.cmake new file mode 100644 index 0000000..dc16ce9 --- /dev/null +++ b/QiliIFW.cmake @@ -0,0 +1,114 @@ +# Copyright (C) 2024 Sauntor + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +## Variables for building +set(QILI_IFW_SRC "${CMAKE_SOURCE_DIR}/IFW") +set(QILI_IFW_BIN "${CMAKE_BINARY_DIR}/IFW") +set(QILI_IFW_PKG "packages/me.sauntor.qili") +# Change the install directory to IFW package's data dir +set(CMAKE_INSTALL_PREFIX "${QILI_IFW_BIN}/${QILI_IFW_PKG}/data") +set(QILI_IFW_EXT "${CMAKE_EXECUTABLE_SUFFIX}") + +if (NOT QILI_IFW_EXT) + set(QILI_IFW_EXT ".bin") +endif() + +## Find IFW binaries +if (DEFINED IFW_PATH) + set(QILI_IFW_PREFIX ${IFW_PATH}) +elseif(NOT $ENV{IFW_PATH}) + set(QILI_IFW_PREFIX $ENV{IFW_PATH}) +endif() +if (NOT QILI_IFW_PREFIX) + message(WARNING "Can't find IFW binaries") +endif() + +find_program(QILI_BINARY_CREATOR + NAMES binarycreator binarycreator.exe binarycreator.app + HINTS "${QILI_IFW_PREFIX}/bin" +) +find_program(QILI_INSTALLER_BASE + NAMES installerbase installerbase.exe installerbase.app + HINTS "${QILI_IFW_PREFIX}/bin" +) + +## Custom commands to build the installer +add_custom_command(OUTPUT Qili_IFW_INIT + COMMAND cmake -E make_directory "${CMAKE_INSTALL_PREFIX}" +) +add_custom_command(OUTPUT Qili_IFW_CLEAN + COMMAND cmake -E remove_directory "${CMAKE_INSTALL_PREFIX}" +) + +add_custom_command(OUTPUT Qili_IFW_copycfg + COMMAND cmake -E copy_directory + "${QILI_IFW_SRC}" + "${QILI_IFW_BIN}" + DEPENDS Qili_IFW_INIT +) +add_custom_command(OUTPUT Qili_IFW_copylic + COMMAND cmake -E copy + "${CMAKE_SOURCE_DIR}/LICENSE" + "${QILI_IFW_BIN}/${QILI_IFW_PKG}/meta" + DEPENDS Qili_IFW_copycfg +) + +add_custom_command(OUTPUT Qili_IFW_instdata + COMMAND cmake + --build . + --target install + DEPENDS Qili_IFW_copycfg + USES_TERMINAL + VERBATIM +) + +add_custom_command(OUTPUT Qili_IFW_createbin + COMMAND "${QILI_BINARY_CREATOR}" + -v + --offline-only + -t "${QILI_INSTALLER_BASE}" + -c "${QILI_IFW_BIN}/config/general.xml" + -p "packages" + "${CMAKE_BINARY_DIR}/${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}${QILI_IFW_EXT}" + WORKING_DIRECTORY "${QILI_IFW_BIN}" + DEPENDS Qili_IFW_copylic Qili_IFW_instdata + USES_TERMINAL + VERBATIM +) +add_custom_target(Qili_IFW_lupdate + COMMAND Qt${QT_VERSION_MAJOR}::lupdate + ${QILI_IFW_SRC}/config/controller.qs + ${QILI_IFW_SRC}/${QILI_IFW_PKG}/meta/desktop.qs + -ts + ${QILI_IFW_SRC}/${QILI_IFW_PKG}/meta/zh_CN.ts +) +add_custom_target(Qili_IFW_lrelease + COMMAND Qt${QT_VERSION_MAJOR}::lrelease + ${QILI_IFW_SRC}/${QILI_IFW_PKG}/meta/zh_CN.ts + -qm + ${QILI_IFW_BIN}/${QILI_IFW_PKG}/meta/zh_CN.qm + DEPENDS Qili_IFW_copycfg +) + +add_custom_target(ifw + DEPENDS + Qili_IFW_INIT + Qili_IFW_copycfg + Qili_IFW_copylic + Qili_IFW_instdata + Qili_IFW_createbin + # Qili_IFW_lrelease + Qili_IFW_CLEAN +) diff --git a/QiliInstall.cmake b/QiliInstall.cmake new file mode 100644 index 0000000..08fbacf --- /dev/null +++ b/QiliInstall.cmake @@ -0,0 +1,33 @@ +# Copyright (C) 2024 Sauntor + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +if (WIN32) + include(InstallRequiredSystemLibraries) +elseif(APPLE) + message(FATAL_ERROR "Not tested on Apple's products, comment out this line if you want take the risk on your own!") +elseif(ANDROID) + message(FATAL_ERROR "Not tested on Android yet, comment out this line if you want take the risk on your own!") +elseif (LINUX OR UNIX) + cmake_host_system_information(RESULT DISTRO QUERY DISTRIB_INFO) + + configure_file( + "${CMAKE_SOURCE_DIR}/Linux/me.sauntor.qili.desktop.in" + "${CMAKE_BINARY_DIR}/me.sauntor.qili.desktop" + @ONLY) + install(FILES ${CMAKE_BINARY_DIR}/me.sauntor.qili.desktop + DESTINATION ${CMAKE_INSTALL_DATADIR}/applications/) +else() + message(FATAL_ERROR "Not tested on ${CMAKE_SYSTEM} yet, comment out this line if you want take the risk on your own!") +endif() diff --git a/README.md b/README.md new file mode 100644 index 0000000..614038d --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# Qili弹幕姬 中文|[English](README_en.md) + +### 介绍 +Qili 是一款免费且开源的B站直播弹幕语音播报软件,主要适配适配 `Windows` 及 `Linux` 平台。 + +### 版权声明 +本软件使用 [GPL v3](https://www.gnu.org/licenses/gpl-3.0.txt) 进行授权。 + +### 软件架构 +语音播报功能依赖Qt的TextToSpeech引擎,Qt会利用平台系统的TTS组件, +所以(可能)需要安装一些系统组件。
+本软件采用模拟浏览器登录的方式从B站获取弹幕数据,当然,不登录也可使用基本功能。 + +### 计划实现的功能 +- [] 适配暗色模式 +- [] 历史弹幕存储 +- [] 弹幕数据分析 + +### 已适配系统 +1. Windows 10+ x86_64/amd64 +1. Ubuntu/Kylin 22.04 +2. openSUSE Tumbleweed + +### 安装教程 +##### Windows +1. 下载 [安装包](https://github.com/sauntor/Qili/releases/download/v1.0.0/Qili-1.0.0-Windows-AMD64.exe) +2. 双击软件包,按照引导进行安装 +3. 打开软件并登录(可匿名),在 `设置` 中测试并选择合适的语音 + +##### openSUSE +1. 安装系统语音组件 + ```bash + sudo zypper in --recommends speech-dispatcher speech-dispatcher-configure speech-dispatcher-module-espeak espeak-ng + ``` +2. 下载 [RPM 安装包](https://github.com/sauntor/Qili/releases/download/v1.0.0/me.sauntor.qili-1.0.0-opensuse-tumbleweed.20240105_x86_64.rpm) +3. 单/双击软件包安装,或执行命令
+ ```bash + sudo zypper in me.sauntor.qili-1.0.0-opensuse-tumbleweed.20240105_x86_64.rpm + ``` +4. (可选)编译安装`mbrola`(自行百度) +5. (可选)运行 `spd-conf -u` 按照引导为**当前用户**生成 `speech-dispatcher` 配置 +6. 打开软件并登录(可匿名),在 `设置` 中测试并选择合适的语音 + +##### Ubuntu/Kylin 22.04 +1. 下载 [DEB 安装包](https://github.com/sauntor/Qili/releases/download/v1.0.0/me.sauntor.qili-1.0.0-ubuntu.22.04_x86_64.deb) +2. 打开终端,并用下面的命令安装
+ ```bash + sudo apt install --fix-broken --install-recommends me.sauntor.qili-1.0.0-ubuntu.22.04_x86_64.deb + ``` +3. (可选)`sudo apt install python3-speechd` +4. (可选)运行 `spd-conf -u` 按照引导为**当前用户**生成 `speech-dispatcher` 配置 +5. 打开软件并登录(可匿名),在 `设置` 中测试并选择合适的语音 + +### 使用说明 +1. `房间号` 不限于自己的直播间 +2. 匿名登录可能会接收不到直播间的弹幕数据,(由于B站隐私策略,)即使收到弹幕也无法看到用户全名,但仍能接收到用户进入、本场观众数等信息。 +3. 连接B站成功后,此程序会隐藏到系统托盘 +4. 单击系统托盘图标可显示本场弹幕记录 +5. 双击或中键单击系统托盘图标可以 暂停/继续 语音播报(如果系统支持的话) +6. 本软件不会收集用户的任何信息(纯本地软件,无服务端) + +### 参与贡献 +1. 代码风格:[KDE Code Style](https://community.kde.org/Policies/Frameworks_Coding_Style) +2. 在龙芯等国产硬件上测试并打包本软件 +3. bug修复、帮助实现`计划实现的功能`并`Pull Request` + +### 编译打包 +> 1. 主要依赖:`Qt`(`Widgets` `Network`, `WebSockets` `TextToSpeech`), `brotli` +> 2. 发布时请添加: `-DCMAKE_BUILD_TYPE=Release` 或者 `-DCMAKE_BUILD_TYPE=RelWithDebInfo` + +1. openSUSE Tumbleweed + ``` + # 安装依赖 + sudo zypper in -t pattern devel_C_C++ devel_qt6 + # 下面的命令可能不需要执行 + sudo zypper in --recommends cmake qt6-texttospeech-devel qt6-websockets-devel libbrotli-devel + # 创建编译文件夹 + mkdir build && cd build + # (A)初始化编译配置 + cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -B . -S /path/to/source/of/Qili + # 编译 + cmake --build . --clean-first --verbose --target all + # 打包,在 (A) 处的命令上增加参数: -DUSE_CPACK=ON + cpack --config CPackConfig.cmake -G RPM -V + # Qt Creator集成 + # 将 (A) 处命令换成 + cmake -DDEV_MODE=ON \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_INSTALL_PREFIX=$HOME/QiliDev \ + -B . \ + -S /path/to/source/of/Qili + # 安装 + cmake --build . --clean-first --verbose --target install + # 启动QtCreator + LD_LIBRARY_PATH=$HOME/QiliDev/lib64 \ + QT_PLUGIN_PATH=$HOME/QiliDev/plugins \ + qtcreator + ``` +2. Ubuntu/Kylin 22.04 + ```bash + # 安装依赖 + sudo apt install --install-recommends \ + build-essential \ + cmake \ + qtbase5-dev \ + qtbase5-dev-tools \ + qttools5-dev \ + qttools5-dev-tools \ + libqt5texttospeech5-dev \ + libqt5websockets5-dev \ + libbrotli-dev + # 创建编译文件夹 + mkdir build && cd build + # (A) 初始化编译配置 + cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DUSE_QT5=ON -B . -S /path/to/source/of/Qili + # 编译 + cmake --build . --clean-first --verbose --target all + # 打包,另需在 (A) 处的命令上增加参数: -DUSE_CPACK=ON + cpack --config CPackConfig.cmake -G RPM -V + ``` + +### 赞助 +> 自愿赞助作者工作的,可通过应用内的 `鸣谢` 或下面的二维码捐助 + +- 支付宝
+ +![支付宝](App/images/alipay.png) + +- 微信支付
+ +![微信支付](App/images/wechat.png) + +### 鸣谢 +> 本项目使用或参考了以下项目 +1. https://github.com/SocialSisterYi/bilibili-API-collect +2. https://www.qt.io/ +3. https://github.com/google/brotli diff --git a/README_en.md b/README_en.md new file mode 100644 index 0000000..db74e42 --- /dev/null +++ b/README_en.md @@ -0,0 +1,139 @@ +# Qili [中文](README.md)|English + +### What's it? +Qili is a subtitle spearker for live broadcasting at bilibili.com. + +### License +This software is licensed under [GPL v3](https://www.gnu.org/licenses/gpl-3.0.txt) + +### How it works? +Qili using `Qt`'s `TextToSpeech` engine to speak subtitles out, targeting on Linux platform. +So, you may need to install some extra components provided by your Linux vendor. + +### Schedules +- [] Adopt to Dark Mode +- [] Subtitle Storage +- [] Subtitle Analysis + +### Supported OS +1. Windows 10 x86_64/amd64 +2. Ubuntu/Kylin 22.04 +3. openSUSE Tumbleweed + +### Installation +> Please replace `/path/to/...` with the real path to the file + +##### Windows +1. Download the [Installer](https://github.com/sauntor/Qili/releases/download/v1.0.0/Qili-1.0.0-Windows-AMD64.exe) +2. Double click `Qili-1.0.0-Windows-AMD64.exe` and follow the instructions. +3. Launch Qili, login by scanning QRCode or just as anonymous, test voices using `Qili Settings`, and `Apply` if some one is ok. + +##### openSUSE +> Please replace `/path/to/...` with the real path to the file +1. Install required components + ```bash + sudo zypper in --recommends speech-dispatcher speech-dispatcher-configure speech-dispatcher-module-espeak espeak-ng + ``` +2. Download [Tumbleweed RPM](https://gitee.com/sauntor/Qili/releases/download/v1.0.0/me.sauntor.qili-1.0.0-opensuse-tumbleweed.20240105_x86_64.rpm) +3. Click on the rpm and following the instruction, or run the command with a terminal:
+ ```bash + sudo zypper in ./me.sauntor.qili-1.0.0-opensuse-tumbleweed.20240109_x86_64.rpm + ``` +4. Optional: compile and install `mbrola` from source code, you may search Google for help +5. Optional: run `spd-conf -u` and following the instruction to generate a `speech-dispatcher` config file for you +6. Launch Qili, login by scanning QRCode or just as anonymous, test voices using `Qili Settings`, and `Apply` if some one is ok. + +##### Ubuntu/Kylin 22.04 +1. Download [Ubuntu DEB](https://gitee.com/sauntor/Qili/releases/download/v1.0.0/me.sauntor.qili-1.0.0-Deepin.20.9_x86_64.deb) +2. Open `Terminal`, and run:
+ ```bash + sudo apt install --fix-broken --install-recommends ./me.sauntor.qili-1.0.0-ubuntu.22.04_x86_64.deb + ``` +3. Optional: `sudo apt install python3-speechd` +4. Optional: run `spd-conf -u` and following the instruction to generate a `speech-dispatcher` config file for you +5. Launch Qili, login by scanning QRCode or just as anonymous, test voices using `Qili Settings`, and `Apply` if some one is ok. + +### Usage +1. `ROOM` number is not limited to your own +2. Qili may not receive subtitles from bilibili.com or receive subtitles without user names, if you login as an anonymous, + biblibili controls this on it's privacy. But Qili can still speak it out when someone enter the room. +3. Qili will hide to System Tray after it connected to bilibili.com. +4. Click the tray icon to show the last 100 subtitles. +5. Double click or Middle click to `Pause`/`Resume` (if it supported by your system) +6. Qili does not collection any user's data, and no server side on its own + +### Contribution +1. Following [KDE Code Style](https://community.kde.org/Policies/Frameworks_Coding_Style) +2. Fix bug, migrate, test and/or package Qili for other Linux distributions +3. Working on `Schedules` and make a `Pull Request` + +### Compile & Package +> Please add the following parameter to cmake arguments when Releasing/Packaging:
+> `-DCMAKE_BUILD_TYPE=Release` or `-DCMAKE_BUILD_TYPE=RelWithDebInfo` + +1. openSUSE Tumbleweed + ``` + # Install required components + sudo zypper in -t pattern devel_C_C++ devel_qt6 + # The following line may not needed + sudo zypper in --recommends cmake qt6-texttospeech-devel qt6-websockets-devel libbrotli-devel + # Make a directory for building + mkdir build && cd build + # [1]Setup with cmake + cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -B . -S /path/to/source/of/Qili + # Compile + cmake --build . --clean-first --verbose --target all + # Packaging,you should add -DUSE_CPACK=ON to the arguments on [1], or re-execute [1] with this param + cpack --config CPackConfig.cmake -G RPM -V + # Integrate Custom Widgets with Qt Creator + # replace/execute the command at [1] + cmake -DDEV_MODE=ON \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_INSTALL_PREFIX=$HOME/QiliDev \ + -B . \ + -S /path/to/source/of/Qili + # Install all (include qt designer plugin) components + cmake --build . --clean-first --verbose --target install + # Launch QtCreator + LD_LIBRARY_PATH=$HOME/QiliDev/lib64 \ + QT_PLUGIN_PATH=$HOME/QiliDev/plugins \ + qtcreator + ``` +2. Ubuntu/Kylin 22.04 + ```bash + # Install required components + sudo apt install --install-recommends \ + build-essential \ + cmake \ + qtbase5-dev \ + qtbase5-dev-tools \ + qttools5-dev \ + qttools5-dev-tools \ + libqt5texttospeech5-dev \ + libqt5websockets5-dev \ + libbrotli-dev + # Make a directory for building + mkdir build && cd build + # [1]Setup with cmake + cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DUSE_QT5=ON -B . -S /path/to/source/of/Qili + # Compile + cmake --build . --clean-first --verbose --target all + # Packaging,you should add -DUSE_CPACK=ON to the arguments on [1], or re-execute [1] with this param + cpack --config CPackConfig.cmake -G RPM -V + ``` + +### Sponsor +> You sponsor the author by the `Thanks` menu in `Qill`, or pay via: + +- Alipay
+ +![Alipay](App/images/alipay.png) + +- Wechat Pay
+ +![Wechat Pay](App/images/wechat.png) + +#### Thanks to +1. https://github.com/SocialSisterYi/bilibili-API-collect +2. https://www.qt.io/ +3. https://github.com/google/brotli diff --git a/Thirdparty/CMakeLists.txt b/Thirdparty/CMakeLists.txt new file mode 100644 index 0000000..df3e72d --- /dev/null +++ b/Thirdparty/CMakeLists.txt @@ -0,0 +1,43 @@ +# Copyright (C) 2024 Sauntor + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +################################## +# QR code generation library # +################################## +qili_add_library(QRCodeGen) + +target_include_directories(QRCodeGen PRIVATE + ${CMAKE_BINARY_DIR} + ${CMAKE_CURRENT_SOURCE_DIR} +) +target_sources(QRCodeGen PRIVATE + ThirdpartyExports.h + QiliQRCode.h + QiliQRCode.cpp + qrcodegen.hpp + qrcodegen.cpp +) +target_link_libraries(QRCodeGen PRIVATE Qt::Core) +target_compile_definitions(QRCodeGen PRIVATE QILI_THIRDPARTY_LIBRARY) + +install(TARGETS QRCodeGen + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT Runtime +) + +qili_finalize_target(QRCodeGen) diff --git a/Thirdparty/QiliQRCode.cpp b/Thirdparty/QiliQRCode.cpp new file mode 100644 index 0000000..1799e88 --- /dev/null +++ b/Thirdparty/QiliQRCode.cpp @@ -0,0 +1,48 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliQRCode.h" + +#include + +using namespace qrcodegen; + +struct QRCodeData +{ + QrCode code; +}; + +QiliQRCode::QiliQRCode(const QString &content, QObject *parent) + : QObject{parent} +{ + auto qrcode = QrCode::encodeText(qUtf8Printable(content), QrCode::Ecc::LOW); + mData = new QRCodeData{qrcode}; +} + +QiliQRCode::~QiliQRCode() +{ + delete mData; +} + +int QiliQRCode::size() const +{ + return mData->code.getSize(); +} + +bool QiliQRCode::module(int x, int y) const +{ + return mData->code.getModule(x, y); +} diff --git a/Thirdparty/QiliQRCode.h b/Thirdparty/QiliQRCode.h new file mode 100644 index 0000000..7f45291 --- /dev/null +++ b/Thirdparty/QiliQRCode.h @@ -0,0 +1,42 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILIQRCODE_H +#define QILIQRCODE_H + +#include "ThirdpartyExports.h" + +#include + +struct QRCodeData; + +class QILI_THIRDPARTY_EXPORT QiliQRCode : public QObject +{ + Q_OBJECT + +public: + explicit QiliQRCode(const QString &content, QObject *parent = nullptr); + ~QiliQRCode(); + + int size() const; + bool module(int x, int y) const; + +private: + QRCodeData *mData; + +}; + +#endif // QILIQRCODE_H diff --git a/Thirdparty/ThirdpartyExports.h b/Thirdparty/ThirdpartyExports.h new file mode 100644 index 0000000..1ef3caa --- /dev/null +++ b/Thirdparty/ThirdpartyExports.h @@ -0,0 +1,26 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef THIRDPARTYGLOBAL_H +#define THIRDPARTYGLOBAL_H + +#if defined(QILI_THIRDPARTY_LIBRARY) +#define QILI_THIRDPARTY_EXPORT Q_DECL_EXPORT +#else +#define QILI_THIRDPARTY_EXPORT Q_DECL_IMPORT +#endif + +#endif // THIRDPARTYGLOBAL_H diff --git a/Thirdparty/qrcodegen.cpp b/Thirdparty/qrcodegen.cpp new file mode 100644 index 0000000..0957b79 --- /dev/null +++ b/Thirdparty/qrcodegen.cpp @@ -0,0 +1,830 @@ +/* + * QR Code generator library (C++) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "qrcodegen.hpp" + +using std::int8_t; +using std::uint8_t; +using std::size_t; +using std::vector; + + +namespace qrcodegen { + +/*---- Class QrSegment ----*/ + +QrSegment::Mode::Mode(int mode, int cc0, int cc1, int cc2) : + modeBits(mode) { + numBitsCharCount[0] = cc0; + numBitsCharCount[1] = cc1; + numBitsCharCount[2] = cc2; +} + + +int QrSegment::Mode::getModeBits() const { + return modeBits; +} + + +int QrSegment::Mode::numCharCountBits(int ver) const { + return numBitsCharCount[(ver + 7) / 17]; +} + + +const QrSegment::Mode QrSegment::Mode::NUMERIC (0x1, 10, 12, 14); +const QrSegment::Mode QrSegment::Mode::ALPHANUMERIC(0x2, 9, 11, 13); +const QrSegment::Mode QrSegment::Mode::BYTE (0x4, 8, 16, 16); +const QrSegment::Mode QrSegment::Mode::KANJI (0x8, 8, 10, 12); +const QrSegment::Mode QrSegment::Mode::ECI (0x7, 0, 0, 0); + + +QrSegment QrSegment::makeBytes(const vector &data) { + if (data.size() > static_cast(INT_MAX)) + throw std::length_error("Data too long"); + BitBuffer bb; + for (uint8_t b : data) + bb.appendBits(b, 8); + return QrSegment(Mode::BYTE, static_cast(data.size()), std::move(bb)); +} + + +QrSegment QrSegment::makeNumeric(const char *digits) { + BitBuffer bb; + int accumData = 0; + int accumCount = 0; + int charCount = 0; + for (; *digits != '\0'; digits++, charCount++) { + char c = *digits; + if (c < '0' || c > '9') + throw std::domain_error("String contains non-numeric characters"); + accumData = accumData * 10 + (c - '0'); + accumCount++; + if (accumCount == 3) { + bb.appendBits(static_cast(accumData), 10); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 or 2 digits remaining + bb.appendBits(static_cast(accumData), accumCount * 3 + 1); + return QrSegment(Mode::NUMERIC, charCount, std::move(bb)); +} + + +QrSegment QrSegment::makeAlphanumeric(const char *text) { + BitBuffer bb; + int accumData = 0; + int accumCount = 0; + int charCount = 0; + for (; *text != '\0'; text++, charCount++) { + const char *temp = std::strchr(ALPHANUMERIC_CHARSET, *text); + if (temp == nullptr) + throw std::domain_error("String contains unencodable characters in alphanumeric mode"); + accumData = accumData * 45 + static_cast(temp - ALPHANUMERIC_CHARSET); + accumCount++; + if (accumCount == 2) { + bb.appendBits(static_cast(accumData), 11); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 character remaining + bb.appendBits(static_cast(accumData), 6); + return QrSegment(Mode::ALPHANUMERIC, charCount, std::move(bb)); +} + + +vector QrSegment::makeSegments(const char *text) { + // Select the most efficient segment encoding automatically + vector result; + if (*text == '\0'); // Leave result empty + else if (isNumeric(text)) + result.push_back(makeNumeric(text)); + else if (isAlphanumeric(text)) + result.push_back(makeAlphanumeric(text)); + else { + vector bytes; + for (; *text != '\0'; text++) + bytes.push_back(static_cast(*text)); + result.push_back(makeBytes(bytes)); + } + return result; +} + + +QrSegment QrSegment::makeEci(long assignVal) { + BitBuffer bb; + if (assignVal < 0) + throw std::domain_error("ECI assignment value out of range"); + else if (assignVal < (1 << 7)) + bb.appendBits(static_cast(assignVal), 8); + else if (assignVal < (1 << 14)) { + bb.appendBits(2, 2); + bb.appendBits(static_cast(assignVal), 14); + } else if (assignVal < 1000000L) { + bb.appendBits(6, 3); + bb.appendBits(static_cast(assignVal), 21); + } else + throw std::domain_error("ECI assignment value out of range"); + return QrSegment(Mode::ECI, 0, std::move(bb)); +} + + +QrSegment::QrSegment(const Mode &md, int numCh, const std::vector &dt) : + mode(&md), + numChars(numCh), + data(dt) { + if (numCh < 0) + throw std::domain_error("Invalid value"); +} + + +QrSegment::QrSegment(const Mode &md, int numCh, std::vector &&dt) : + mode(&md), + numChars(numCh), + data(std::move(dt)) { + if (numCh < 0) + throw std::domain_error("Invalid value"); +} + + +int QrSegment::getTotalBits(const vector &segs, int version) { + int result = 0; + for (const QrSegment &seg : segs) { + int ccbits = seg.mode->numCharCountBits(version); + if (seg.numChars >= (1L << ccbits)) + return -1; // The segment's length doesn't fit the field's bit width + if (4 + ccbits > INT_MAX - result) + return -1; // The sum will overflow an int type + result += 4 + ccbits; + if (seg.data.size() > static_cast(INT_MAX - result)) + return -1; // The sum will overflow an int type + result += static_cast(seg.data.size()); + } + return result; +} + + +bool QrSegment::isNumeric(const char *text) { + for (; *text != '\0'; text++) { + char c = *text; + if (c < '0' || c > '9') + return false; + } + return true; +} + + +bool QrSegment::isAlphanumeric(const char *text) { + for (; *text != '\0'; text++) { + if (std::strchr(ALPHANUMERIC_CHARSET, *text) == nullptr) + return false; + } + return true; +} + + +const QrSegment::Mode &QrSegment::getMode() const { + return *mode; +} + + +int QrSegment::getNumChars() const { + return numChars; +} + + +const std::vector &QrSegment::getData() const { + return data; +} + + +const char *QrSegment::ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; + + + +/*---- Class QrCode ----*/ + +int QrCode::getFormatBits(Ecc ecl) { + switch (ecl) { + case Ecc::LOW : return 1; + case Ecc::MEDIUM : return 0; + case Ecc::QUARTILE: return 3; + case Ecc::HIGH : return 2; + default: throw std::logic_error("Unreachable"); + } +} + + +QrCode QrCode::encodeText(const char *text, Ecc ecl) { + vector segs = QrSegment::makeSegments(text); + return encodeSegments(segs, ecl); +} + + +QrCode QrCode::encodeBinary(const vector &data, Ecc ecl) { + vector segs{QrSegment::makeBytes(data)}; + return encodeSegments(segs, ecl); +} + + +QrCode QrCode::encodeSegments(const vector &segs, Ecc ecl, + int minVersion, int maxVersion, int mask, bool boostEcl) { + if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION) || mask < -1 || mask > 7) + throw std::invalid_argument("Invalid value"); + + // Find the minimal version number to use + int version, dataUsedBits; + for (version = minVersion; ; version++) { + int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available + dataUsedBits = QrSegment::getTotalBits(segs, version); + if (dataUsedBits != -1 && dataUsedBits <= dataCapacityBits) + break; // This version number is found to be suitable + if (version >= maxVersion) { // All versions in the range could not fit the given data + std::ostringstream sb; + if (dataUsedBits == -1) + sb << "Segment too long"; + else { + sb << "Data length = " << dataUsedBits << " bits, "; + sb << "Max capacity = " << dataCapacityBits << " bits"; + } + throw data_too_long(sb.str()); + } + } + assert(dataUsedBits != -1); + + // Increase the error correction level while the data still fits in the current version number + for (Ecc newEcl : {Ecc::MEDIUM, Ecc::QUARTILE, Ecc::HIGH}) { // From low to high + if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8) + ecl = newEcl; + } + + // Concatenate all segments to create the data bit string + BitBuffer bb; + for (const QrSegment &seg : segs) { + bb.appendBits(static_cast(seg.getMode().getModeBits()), 4); + bb.appendBits(static_cast(seg.getNumChars()), seg.getMode().numCharCountBits(version)); + bb.insert(bb.end(), seg.getData().begin(), seg.getData().end()); + } + assert(bb.size() == static_cast(dataUsedBits)); + + // Add terminator and pad up to a byte if applicable + size_t dataCapacityBits = static_cast(getNumDataCodewords(version, ecl)) * 8; + assert(bb.size() <= dataCapacityBits); + bb.appendBits(0, std::min(4, static_cast(dataCapacityBits - bb.size()))); + bb.appendBits(0, (8 - static_cast(bb.size() % 8)) % 8); + assert(bb.size() % 8 == 0); + + // Pad with alternating bytes until data capacity is reached + for (uint8_t padByte = 0xEC; bb.size() < dataCapacityBits; padByte ^= 0xEC ^ 0x11) + bb.appendBits(padByte, 8); + + // Pack bits into bytes in big endian + vector dataCodewords(bb.size() / 8); + for (size_t i = 0; i < bb.size(); i++) + dataCodewords.at(i >> 3) |= (bb.at(i) ? 1 : 0) << (7 - (i & 7)); + + // Create the QR Code object + return QrCode(version, ecl, dataCodewords, mask); +} + + +QrCode::QrCode(int ver, Ecc ecl, const vector &dataCodewords, int msk) : + // Initialize fields and check arguments + version(ver), + errorCorrectionLevel(ecl) { + if (ver < MIN_VERSION || ver > MAX_VERSION) + throw std::domain_error("Version value out of range"); + if (msk < -1 || msk > 7) + throw std::domain_error("Mask value out of range"); + size = ver * 4 + 17; + size_t sz = static_cast(size); + modules = vector >(sz, vector(sz)); // Initially all light + isFunction = vector >(sz, vector(sz)); + + // Compute ECC, draw modules + drawFunctionPatterns(); + const vector allCodewords = addEccAndInterleave(dataCodewords); + drawCodewords(allCodewords); + + // Do masking + if (msk == -1) { // Automatically choose best mask + long minPenalty = LONG_MAX; + for (int i = 0; i < 8; i++) { + applyMask(i); + drawFormatBits(i); + long penalty = getPenaltyScore(); + if (penalty < minPenalty) { + msk = i; + minPenalty = penalty; + } + applyMask(i); // Undoes the mask due to XOR + } + } + assert(0 <= msk && msk <= 7); + mask = msk; + applyMask(msk); // Apply the final choice of mask + drawFormatBits(msk); // Overwrite old format bits + + isFunction.clear(); + isFunction.shrink_to_fit(); +} + + +int QrCode::getVersion() const { + return version; +} + + +int QrCode::getSize() const { + return size; +} + + +QrCode::Ecc QrCode::getErrorCorrectionLevel() const { + return errorCorrectionLevel; +} + + +int QrCode::getMask() const { + return mask; +} + + +bool QrCode::getModule(int x, int y) const { + return 0 <= x && x < size && 0 <= y && y < size && module(x, y); +} + + +void QrCode::drawFunctionPatterns() { + // Draw horizontal and vertical timing patterns + for (int i = 0; i < size; i++) { + setFunctionModule(6, i, i % 2 == 0); + setFunctionModule(i, 6, i % 2 == 0); + } + + // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules) + drawFinderPattern(3, 3); + drawFinderPattern(size - 4, 3); + drawFinderPattern(3, size - 4); + + // Draw numerous alignment patterns + const vector alignPatPos = getAlignmentPatternPositions(); + size_t numAlign = alignPatPos.size(); + for (size_t i = 0; i < numAlign; i++) { + for (size_t j = 0; j < numAlign; j++) { + // Don't draw on the three finder corners + if (!((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0))) + drawAlignmentPattern(alignPatPos.at(i), alignPatPos.at(j)); + } + } + + // Draw configuration data + drawFormatBits(0); // Dummy mask value; overwritten later in the constructor + drawVersion(); +} + + +void QrCode::drawFormatBits(int msk) { + // Calculate error correction code and pack bits + int data = getFormatBits(errorCorrectionLevel) << 3 | msk; // errCorrLvl is uint2, msk is uint3 + int rem = data; + for (int i = 0; i < 10; i++) + rem = (rem << 1) ^ ((rem >> 9) * 0x537); + int bits = (data << 10 | rem) ^ 0x5412; // uint15 + assert(bits >> 15 == 0); + + // Draw first copy + for (int i = 0; i <= 5; i++) + setFunctionModule(8, i, getBit(bits, i)); + setFunctionModule(8, 7, getBit(bits, 6)); + setFunctionModule(8, 8, getBit(bits, 7)); + setFunctionModule(7, 8, getBit(bits, 8)); + for (int i = 9; i < 15; i++) + setFunctionModule(14 - i, 8, getBit(bits, i)); + + // Draw second copy + for (int i = 0; i < 8; i++) + setFunctionModule(size - 1 - i, 8, getBit(bits, i)); + for (int i = 8; i < 15; i++) + setFunctionModule(8, size - 15 + i, getBit(bits, i)); + setFunctionModule(8, size - 8, true); // Always dark +} + + +void QrCode::drawVersion() { + if (version < 7) + return; + + // Calculate error correction code and pack bits + int rem = version; // version is uint6, in the range [7, 40] + for (int i = 0; i < 12; i++) + rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); + long bits = static_cast(version) << 12 | rem; // uint18 + assert(bits >> 18 == 0); + + // Draw two copies + for (int i = 0; i < 18; i++) { + bool bit = getBit(bits, i); + int a = size - 11 + i % 3; + int b = i / 3; + setFunctionModule(a, b, bit); + setFunctionModule(b, a, bit); + } +} + + +void QrCode::drawFinderPattern(int x, int y) { + for (int dy = -4; dy <= 4; dy++) { + for (int dx = -4; dx <= 4; dx++) { + int dist = std::max(std::abs(dx), std::abs(dy)); // Chebyshev/infinity norm + int xx = x + dx, yy = y + dy; + if (0 <= xx && xx < size && 0 <= yy && yy < size) + setFunctionModule(xx, yy, dist != 2 && dist != 4); + } + } +} + + +void QrCode::drawAlignmentPattern(int x, int y) { + for (int dy = -2; dy <= 2; dy++) { + for (int dx = -2; dx <= 2; dx++) + setFunctionModule(x + dx, y + dy, std::max(std::abs(dx), std::abs(dy)) != 1); + } +} + + +void QrCode::setFunctionModule(int x, int y, bool isDark) { + size_t ux = static_cast(x); + size_t uy = static_cast(y); + modules .at(uy).at(ux) = isDark; + isFunction.at(uy).at(ux) = true; +} + + +bool QrCode::module(int x, int y) const { + return modules.at(static_cast(y)).at(static_cast(x)); +} + + +vector QrCode::addEccAndInterleave(const vector &data) const { + if (data.size() != static_cast(getNumDataCodewords(version, errorCorrectionLevel))) + throw std::invalid_argument("Invalid argument"); + + // Calculate parameter numbers + int numBlocks = NUM_ERROR_CORRECTION_BLOCKS[static_cast(errorCorrectionLevel)][version]; + int blockEccLen = ECC_CODEWORDS_PER_BLOCK [static_cast(errorCorrectionLevel)][version]; + int rawCodewords = getNumRawDataModules(version) / 8; + int numShortBlocks = numBlocks - rawCodewords % numBlocks; + int shortBlockLen = rawCodewords / numBlocks; + + // Split data into blocks and append ECC to each block + vector > blocks; + const vector rsDiv = reedSolomonComputeDivisor(blockEccLen); + for (int i = 0, k = 0; i < numBlocks; i++) { + vector dat(data.cbegin() + k, data.cbegin() + (k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1))); + k += static_cast(dat.size()); + const vector ecc = reedSolomonComputeRemainder(dat, rsDiv); + if (i < numShortBlocks) + dat.push_back(0); + dat.insert(dat.end(), ecc.cbegin(), ecc.cend()); + blocks.push_back(std::move(dat)); + } + + // Interleave (not concatenate) the bytes from every block into a single sequence + vector result; + for (size_t i = 0; i < blocks.at(0).size(); i++) { + for (size_t j = 0; j < blocks.size(); j++) { + // Skip the padding byte in short blocks + if (i != static_cast(shortBlockLen - blockEccLen) || j >= static_cast(numShortBlocks)) + result.push_back(blocks.at(j).at(i)); + } + } + assert(result.size() == static_cast(rawCodewords)); + return result; +} + + +void QrCode::drawCodewords(const vector &data) { + if (data.size() != static_cast(getNumRawDataModules(version) / 8)) + throw std::invalid_argument("Invalid argument"); + + size_t i = 0; // Bit index into the data + // Do the funny zigzag scan + for (int right = size - 1; right >= 1; right -= 2) { // Index of right column in each column pair + if (right == 6) + right = 5; + for (int vert = 0; vert < size; vert++) { // Vertical counter + for (int j = 0; j < 2; j++) { + size_t x = static_cast(right - j); // Actual x coordinate + bool upward = ((right + 1) & 2) == 0; + size_t y = static_cast(upward ? size - 1 - vert : vert); // Actual y coordinate + if (!isFunction.at(y).at(x) && i < data.size() * 8) { + modules.at(y).at(x) = getBit(data.at(i >> 3), 7 - static_cast(i & 7)); + i++; + } + // If this QR Code has any remainder bits (0 to 7), they were assigned as + // 0/false/light by the constructor and are left unchanged by this method + } + } + } + assert(i == data.size() * 8); +} + + +void QrCode::applyMask(int msk) { + if (msk < 0 || msk > 7) + throw std::domain_error("Mask value out of range"); + size_t sz = static_cast(size); + for (size_t y = 0; y < sz; y++) { + for (size_t x = 0; x < sz; x++) { + bool invert; + switch (msk) { + case 0: invert = (x + y) % 2 == 0; break; + case 1: invert = y % 2 == 0; break; + case 2: invert = x % 3 == 0; break; + case 3: invert = (x + y) % 3 == 0; break; + case 4: invert = (x / 3 + y / 2) % 2 == 0; break; + case 5: invert = x * y % 2 + x * y % 3 == 0; break; + case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break; + case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break; + default: throw std::logic_error("Unreachable"); + } + modules.at(y).at(x) = modules.at(y).at(x) ^ (invert & !isFunction.at(y).at(x)); + } + } +} + + +long QrCode::getPenaltyScore() const { + long result = 0; + + // Adjacent modules in row having same color, and finder-like patterns + for (int y = 0; y < size; y++) { + bool runColor = false; + int runX = 0; + std::array runHistory = {}; + for (int x = 0; x < size; x++) { + if (module(x, y) == runColor) { + runX++; + if (runX == 5) + result += PENALTY_N1; + else if (runX > 5) + result++; + } else { + finderPenaltyAddHistory(runX, runHistory); + if (!runColor) + result += finderPenaltyCountPatterns(runHistory) * PENALTY_N3; + runColor = module(x, y); + runX = 1; + } + } + result += finderPenaltyTerminateAndCount(runColor, runX, runHistory) * PENALTY_N3; + } + // Adjacent modules in column having same color, and finder-like patterns + for (int x = 0; x < size; x++) { + bool runColor = false; + int runY = 0; + std::array runHistory = {}; + for (int y = 0; y < size; y++) { + if (module(x, y) == runColor) { + runY++; + if (runY == 5) + result += PENALTY_N1; + else if (runY > 5) + result++; + } else { + finderPenaltyAddHistory(runY, runHistory); + if (!runColor) + result += finderPenaltyCountPatterns(runHistory) * PENALTY_N3; + runColor = module(x, y); + runY = 1; + } + } + result += finderPenaltyTerminateAndCount(runColor, runY, runHistory) * PENALTY_N3; + } + + // 2*2 blocks of modules having same color + for (int y = 0; y < size - 1; y++) { + for (int x = 0; x < size - 1; x++) { + bool color = module(x, y); + if ( color == module(x + 1, y) && + color == module(x, y + 1) && + color == module(x + 1, y + 1)) + result += PENALTY_N2; + } + } + + // Balance of dark and light modules + int dark = 0; + for (const vector &row : modules) { + for (bool color : row) { + if (color) + dark++; + } + } + int total = size * size; // Note that size is odd, so dark/total != 1/2 + // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)% + int k = static_cast((std::abs(dark * 20L - total * 10L) + total - 1) / total) - 1; + assert(0 <= k && k <= 9); + result += k * PENALTY_N4; + assert(0 <= result && result <= 2568888L); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4 + return result; +} + + +vector QrCode::getAlignmentPatternPositions() const { + if (version == 1) + return vector(); + else { + int numAlign = version / 7 + 2; + int step = (version == 32) ? 26 : + (version * 4 + numAlign * 2 + 1) / (numAlign * 2 - 2) * 2; + vector result; + for (int i = 0, pos = size - 7; i < numAlign - 1; i++, pos -= step) + result.insert(result.begin(), pos); + result.insert(result.begin(), 6); + return result; + } +} + + +int QrCode::getNumRawDataModules(int ver) { + if (ver < MIN_VERSION || ver > MAX_VERSION) + throw std::domain_error("Version number out of range"); + int result = (16 * ver + 128) * ver + 64; + if (ver >= 2) { + int numAlign = ver / 7 + 2; + result -= (25 * numAlign - 10) * numAlign - 55; + if (ver >= 7) + result -= 36; + } + assert(208 <= result && result <= 29648); + return result; +} + + +int QrCode::getNumDataCodewords(int ver, Ecc ecl) { + return getNumRawDataModules(ver) / 8 + - ECC_CODEWORDS_PER_BLOCK [static_cast(ecl)][ver] + * NUM_ERROR_CORRECTION_BLOCKS[static_cast(ecl)][ver]; +} + + +vector QrCode::reedSolomonComputeDivisor(int degree) { + if (degree < 1 || degree > 255) + throw std::domain_error("Degree out of range"); + // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1. + // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array {255, 8, 93}. + vector result(static_cast(degree)); + result.at(result.size() - 1) = 1; // Start off with the monomial x^0 + + // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}), + // and drop the highest monomial term which is always 1x^degree. + // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D). + uint8_t root = 1; + for (int i = 0; i < degree; i++) { + // Multiply the current product by (x - r^i) + for (size_t j = 0; j < result.size(); j++) { + result.at(j) = reedSolomonMultiply(result.at(j), root); + if (j + 1 < result.size()) + result.at(j) ^= result.at(j + 1); + } + root = reedSolomonMultiply(root, 0x02); + } + return result; +} + + +vector QrCode::reedSolomonComputeRemainder(const vector &data, const vector &divisor) { + vector result(divisor.size()); + for (uint8_t b : data) { // Polynomial division + uint8_t factor = b ^ result.at(0); + result.erase(result.begin()); + result.push_back(0); + for (size_t i = 0; i < result.size(); i++) + result.at(i) ^= reedSolomonMultiply(divisor.at(i), factor); + } + return result; +} + + +uint8_t QrCode::reedSolomonMultiply(uint8_t x, uint8_t y) { + // Russian peasant multiplication + int z = 0; + for (int i = 7; i >= 0; i--) { + z = (z << 1) ^ ((z >> 7) * 0x11D); + z ^= ((y >> i) & 1) * x; + } + assert(z >> 8 == 0); + return static_cast(z); +} + + +int QrCode::finderPenaltyCountPatterns(const std::array &runHistory) const { + int n = runHistory.at(1); + assert(n <= size * 3); + bool core = n > 0 && runHistory.at(2) == n && runHistory.at(3) == n * 3 && runHistory.at(4) == n && runHistory.at(5) == n; + return (core && runHistory.at(0) >= n * 4 && runHistory.at(6) >= n ? 1 : 0) + + (core && runHistory.at(6) >= n * 4 && runHistory.at(0) >= n ? 1 : 0); +} + + +int QrCode::finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, std::array &runHistory) const { + if (currentRunColor) { // Terminate dark run + finderPenaltyAddHistory(currentRunLength, runHistory); + currentRunLength = 0; + } + currentRunLength += size; // Add light border to final run + finderPenaltyAddHistory(currentRunLength, runHistory); + return finderPenaltyCountPatterns(runHistory); +} + + +void QrCode::finderPenaltyAddHistory(int currentRunLength, std::array &runHistory) const { + if (runHistory.at(0) == 0) + currentRunLength += size; // Add light border to initial run + std::copy_backward(runHistory.cbegin(), runHistory.cend() - 1, runHistory.end()); + runHistory.at(0) = currentRunLength; +} + + +bool QrCode::getBit(long x, int i) { + return ((x >> i) & 1) != 0; +} + + +/*---- Tables of constants ----*/ + +const int QrCode::PENALTY_N1 = 3; +const int QrCode::PENALTY_N2 = 3; +const int QrCode::PENALTY_N3 = 40; +const int QrCode::PENALTY_N4 = 10; + + +const int8_t QrCode::ECC_CODEWORDS_PER_BLOCK[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Low + {-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28}, // Medium + {-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Quartile + {-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // High +}; + +const int8_t QrCode::NUM_ERROR_CORRECTION_BLOCKS[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal value) + //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level + {-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low + {-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium + {-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile + {-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High +}; + + +data_too_long::data_too_long(const std::string &msg) : + std::length_error(msg) {} + + + +/*---- Class BitBuffer ----*/ + +BitBuffer::BitBuffer() + : std::vector() {} + + +void BitBuffer::appendBits(std::uint32_t val, int len) { + if (len < 0 || len > 31 || val >> len != 0) + throw std::domain_error("Value out of range"); + for (int i = len - 1; i >= 0; i--) // Append bit by bit + this->push_back(((val >> i) & 1) != 0); +} + +} diff --git a/Thirdparty/qrcodegen.hpp b/Thirdparty/qrcodegen.hpp new file mode 100644 index 0000000..9448982 --- /dev/null +++ b/Thirdparty/qrcodegen.hpp @@ -0,0 +1,549 @@ +/* + * QR Code generator library (C++) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising from, + * out of or in connection with the Software or the use or other dealings in the + * Software. + */ + +#pragma once + +#include +#include +#include +#include +#include + + +namespace qrcodegen { + +/* + * A segment of character/binary/control data in a QR Code symbol. + * Instances of this class are immutable. + * The mid-level way to create a segment is to take the payload data + * and call a static factory function such as QrSegment::makeNumeric(). + * The low-level way to create a segment is to custom-make the bit buffer + * and call the QrSegment() constructor with appropriate values. + * This segment class imposes no length restrictions, but QR Codes have restrictions. + * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data. + * Any segment longer than this is meaningless for the purpose of generating QR Codes. + */ +class QrSegment final { + + /*---- Public helper enumeration ----*/ + + /* + * Describes how a segment's data bits are interpreted. Immutable. + */ + public: class Mode final { + + /*-- Constants --*/ + + public: static const Mode NUMERIC; + public: static const Mode ALPHANUMERIC; + public: static const Mode BYTE; + public: static const Mode KANJI; + public: static const Mode ECI; + + + /*-- Fields --*/ + + // The mode indicator bits, which is a uint4 value (range 0 to 15). + private: int modeBits; + + // Number of character count bits for three different version ranges. + private: int numBitsCharCount[3]; + + + /*-- Constructor --*/ + + private: Mode(int mode, int cc0, int cc1, int cc2); + + + /*-- Methods --*/ + + /* + * (Package-private) Returns the mode indicator bits, which is an unsigned 4-bit value (range 0 to 15). + */ + public: int getModeBits() const; + + /* + * (Package-private) Returns the bit width of the character count field for a segment in + * this mode in a QR Code at the given version number. The result is in the range [0, 16]. + */ + public: int numCharCountBits(int ver) const; + + }; + + + + /*---- Static factory functions (mid level) ----*/ + + /* + * Returns a segment representing the given binary data encoded in + * byte mode. All input byte vectors are acceptable. Any text string + * can be converted to UTF-8 bytes and encoded as a byte mode segment. + */ + public: static QrSegment makeBytes(const std::vector &data); + + + /* + * Returns a segment representing the given string of decimal digits encoded in numeric mode. + */ + public: static QrSegment makeNumeric(const char *digits); + + + /* + * Returns a segment representing the given text string encoded in alphanumeric mode. + * The characters allowed are: 0 to 9, A to Z (uppercase only), space, + * dollar, percent, asterisk, plus, hyphen, period, slash, colon. + */ + public: static QrSegment makeAlphanumeric(const char *text); + + + /* + * Returns a list of zero or more segments to represent the given text string. The result + * may use various segment modes and switch modes to optimize the length of the bit stream. + */ + public: static std::vector makeSegments(const char *text); + + + /* + * Returns a segment representing an Extended Channel Interpretation + * (ECI) designator with the given assignment value. + */ + public: static QrSegment makeEci(long assignVal); + + + /*---- Public static helper functions ----*/ + + /* + * Tests whether the given string can be encoded as a segment in numeric mode. + * A string is encodable iff each character is in the range 0 to 9. + */ + public: static bool isNumeric(const char *text); + + + /* + * Tests whether the given string can be encoded as a segment in alphanumeric mode. + * A string is encodable iff each character is in the following set: 0 to 9, A to Z + * (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon. + */ + public: static bool isAlphanumeric(const char *text); + + + + /*---- Instance fields ----*/ + + /* The mode indicator of this segment. Accessed through getMode(). */ + private: const Mode *mode; + + /* The length of this segment's unencoded data. Measured in characters for + * numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode. + * Always zero or positive. Not the same as the data's bit length. + * Accessed through getNumChars(). */ + private: int numChars; + + /* The data bits of this segment. Accessed through getData(). */ + private: std::vector data; + + + /*---- Constructors (low level) ----*/ + + /* + * Creates a new QR Code segment with the given attributes and data. + * The character count (numCh) must agree with the mode and the bit buffer length, + * but the constraint isn't checked. The given bit buffer is copied and stored. + */ + public: QrSegment(const Mode &md, int numCh, const std::vector &dt); + + + /* + * Creates a new QR Code segment with the given parameters and data. + * The character count (numCh) must agree with the mode and the bit buffer length, + * but the constraint isn't checked. The given bit buffer is moved and stored. + */ + public: QrSegment(const Mode &md, int numCh, std::vector &&dt); + + + /*---- Methods ----*/ + + /* + * Returns the mode field of this segment. + */ + public: const Mode &getMode() const; + + + /* + * Returns the character count field of this segment. + */ + public: int getNumChars() const; + + + /* + * Returns the data bits of this segment. + */ + public: const std::vector &getData() const; + + + // (Package-private) Calculates the number of bits needed to encode the given segments at + // the given version. Returns a non-negative number if successful. Otherwise returns -1 if a + // segment has too many characters to fit its length field, or the total bits exceeds INT_MAX. + public: static int getTotalBits(const std::vector &segs, int version); + + + /*---- Private constant ----*/ + + /* The set of all legal characters in alphanumeric mode, where + * each character value maps to the index in the string. */ + private: static const char *ALPHANUMERIC_CHARSET; + +}; + + + +/* + * A QR Code symbol, which is a type of two-dimension barcode. + * Invented by Denso Wave and described in the ISO/IEC 18004 standard. + * Instances of this class represent an immutable square grid of dark and light cells. + * The class provides static factory functions to create a QR Code from text or binary data. + * The class covers the QR Code Model 2 specification, supporting all versions (sizes) + * from 1 to 40, all 4 error correction levels, and 4 character encoding modes. + * + * Ways to create a QR Code object: + * - High level: Take the payload data and call QrCode::encodeText() or QrCode::encodeBinary(). + * - Mid level: Custom-make the list of segments and call QrCode::encodeSegments(). + * - Low level: Custom-make the array of data codeword bytes (including + * segment headers and final padding, excluding error correction codewords), + * supply the appropriate version number, and call the QrCode() constructor. + * (Note that all ways require supplying the desired error correction level.) + */ +class QrCode final { + + /*---- Public helper enumeration ----*/ + + /* + * The error correction level in a QR Code symbol. + */ + public: enum class Ecc { + LOW = 0 , // The QR Code can tolerate about 7% erroneous codewords + MEDIUM , // The QR Code can tolerate about 15% erroneous codewords + QUARTILE, // The QR Code can tolerate about 25% erroneous codewords + HIGH , // The QR Code can tolerate about 30% erroneous codewords + }; + + + // Returns a value in the range 0 to 3 (unsigned 2-bit integer). + private: static int getFormatBits(Ecc ecl); + + + + /*---- Static factory functions (high level) ----*/ + + /* + * Returns a QR Code representing the given Unicode text string at the given error correction level. + * As a conservative upper bound, this function is guaranteed to succeed for strings that have 2953 or fewer + * UTF-8 code units (not Unicode code points) if the low error correction level is used. The smallest possible + * QR Code version is automatically chosen for the output. The ECC level of the result may be higher than + * the ecl argument if it can be done without increasing the version. + */ + public: static QrCode encodeText(const char *text, Ecc ecl); + + + /* + * Returns a QR Code representing the given binary data at the given error correction level. + * This function always encodes using the binary segment mode, not any text mode. The maximum number of + * bytes allowed is 2953. The smallest possible QR Code version is automatically chosen for the output. + * The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version. + */ + public: static QrCode encodeBinary(const std::vector &data, Ecc ecl); + + + /*---- Static factory functions (mid level) ----*/ + + /* + * Returns a QR Code representing the given segments with the given encoding parameters. + * The smallest possible QR Code version within the given range is automatically + * chosen for the output. Iff boostEcl is true, then the ECC level of the result + * may be higher than the ecl argument if it can be done without increasing the + * version. The mask number is either between 0 to 7 (inclusive) to force that + * mask, or -1 to automatically choose an appropriate mask (which may be slow). + * This function allows the user to create a custom sequence of segments that switches + * between modes (such as alphanumeric and byte) to encode text in less space. + * This is a mid-level API; the high-level API is encodeText() and encodeBinary(). + */ + public: static QrCode encodeSegments(const std::vector &segs, Ecc ecl, + int minVersion=1, int maxVersion=40, int mask=-1, bool boostEcl=true); // All optional parameters + + + + /*---- Instance fields ----*/ + + // Immutable scalar parameters: + + /* The version number of this QR Code, which is between 1 and 40 (inclusive). + * This determines the size of this barcode. */ + private: int version; + + /* The width and height of this QR Code, measured in modules, between + * 21 and 177 (inclusive). This is equal to version * 4 + 17. */ + private: int size; + + /* The error correction level used in this QR Code. */ + private: Ecc errorCorrectionLevel; + + /* The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive). + * Even if a QR Code is created with automatic masking requested (mask = -1), + * the resulting object still has a mask value between 0 and 7. */ + private: int mask; + + // Private grids of modules/pixels, with dimensions of size*size: + + // The modules of this QR Code (false = light, true = dark). + // Immutable after constructor finishes. Accessed through getModule(). + private: std::vector > modules; + + // Indicates function modules that are not subjected to masking. Discarded when constructor finishes. + private: std::vector > isFunction; + + + + /*---- Constructor (low level) ----*/ + + /* + * Creates a new QR Code with the given version number, + * error correction level, data codeword bytes, and mask number. + * This is a low-level API that most users should not use directly. + * A mid-level API is the encodeSegments() function. + */ + public: QrCode(int ver, Ecc ecl, const std::vector &dataCodewords, int msk); + + + + /*---- Public instance methods ----*/ + + /* + * Returns this QR Code's version, in the range [1, 40]. + */ + public: int getVersion() const; + + + /* + * Returns this QR Code's size, in the range [21, 177]. + */ + public: int getSize() const; + + + /* + * Returns this QR Code's error correction level. + */ + public: Ecc getErrorCorrectionLevel() const; + + + /* + * Returns this QR Code's mask, in the range [0, 7]. + */ + public: int getMask() const; + + + /* + * Returns the color of the module (pixel) at the given coordinates, which is false + * for light or true for dark. The top left corner has the coordinates (x=0, y=0). + * If the given coordinates are out of bounds, then false (light) is returned. + */ + public: bool getModule(int x, int y) const; + + + + /*---- Private helper methods for constructor: Drawing function modules ----*/ + + // Reads this object's version field, and draws and marks all function modules. + private: void drawFunctionPatterns(); + + + // Draws two copies of the format bits (with its own error correction code) + // based on the given mask and this object's error correction level field. + private: void drawFormatBits(int msk); + + + // Draws two copies of the version bits (with its own error correction code), + // based on this object's version field, iff 7 <= version <= 40. + private: void drawVersion(); + + + // Draws a 9*9 finder pattern including the border separator, + // with the center module at (x, y). Modules can be out of bounds. + private: void drawFinderPattern(int x, int y); + + + // Draws a 5*5 alignment pattern, with the center module + // at (x, y). All modules must be in bounds. + private: void drawAlignmentPattern(int x, int y); + + + // Sets the color of a module and marks it as a function module. + // Only used by the constructor. Coordinates must be in bounds. + private: void setFunctionModule(int x, int y, bool isDark); + + + // Returns the color of the module at the given coordinates, which must be in range. + private: bool module(int x, int y) const; + + + /*---- Private helper methods for constructor: Codewords and masking ----*/ + + // Returns a new byte string representing the given data with the appropriate error correction + // codewords appended to it, based on this object's version and error correction level. + private: std::vector addEccAndInterleave(const std::vector &data) const; + + + // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire + // data area of this QR Code. Function modules need to be marked off before this is called. + private: void drawCodewords(const std::vector &data); + + + // XORs the codeword modules in this QR Code with the given mask pattern. + // The function modules must be marked and the codeword bits must be drawn + // before masking. Due to the arithmetic of XOR, calling applyMask() with + // the same mask value a second time will undo the mask. A final well-formed + // QR Code needs exactly one (not zero, two, etc.) mask applied. + private: void applyMask(int msk); + + + // Calculates and returns the penalty score based on state of this QR Code's current modules. + // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score. + private: long getPenaltyScore() const; + + + + /*---- Private helper functions ----*/ + + // Returns an ascending list of positions of alignment patterns for this version number. + // Each position is in the range [0,177), and are used on both the x and y axes. + // This could be implemented as lookup table of 40 variable-length lists of unsigned bytes. + private: std::vector getAlignmentPatternPositions() const; + + + // Returns the number of data bits that can be stored in a QR Code of the given version number, after + // all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8. + // The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table. + private: static int getNumRawDataModules(int ver); + + + // Returns the number of 8-bit data (i.e. not error correction) codewords contained in any + // QR Code of the given version number and error correction level, with remainder bits discarded. + // This stateless pure function could be implemented as a (40*4)-cell lookup table. + private: static int getNumDataCodewords(int ver, Ecc ecl); + + + // Returns a Reed-Solomon ECC generator polynomial for the given degree. This could be + // implemented as a lookup table over all possible parameter values, instead of as an algorithm. + private: static std::vector reedSolomonComputeDivisor(int degree); + + + // Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials. + private: static std::vector reedSolomonComputeRemainder(const std::vector &data, const std::vector &divisor); + + + // Returns the product of the two given field elements modulo GF(2^8/0x11D). + // All inputs are valid. This could be implemented as a 256*256 lookup table. + private: static std::uint8_t reedSolomonMultiply(std::uint8_t x, std::uint8_t y); + + + // Can only be called immediately after a light run is added, and + // returns either 0, 1, or 2. A helper function for getPenaltyScore(). + private: int finderPenaltyCountPatterns(const std::array &runHistory) const; + + + // Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore(). + private: int finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, std::array &runHistory) const; + + + // Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore(). + private: void finderPenaltyAddHistory(int currentRunLength, std::array &runHistory) const; + + + // Returns true iff the i'th bit of x is set to 1. + private: static bool getBit(long x, int i); + + + /*---- Constants and tables ----*/ + + // The minimum version number supported in the QR Code Model 2 standard. + public: static constexpr int MIN_VERSION = 1; + + // The maximum version number supported in the QR Code Model 2 standard. + public: static constexpr int MAX_VERSION = 40; + + + // For use in getPenaltyScore(), when evaluating which mask is best. + private: static const int PENALTY_N1; + private: static const int PENALTY_N2; + private: static const int PENALTY_N3; + private: static const int PENALTY_N4; + + + private: static const std::int8_t ECC_CODEWORDS_PER_BLOCK[4][41]; + private: static const std::int8_t NUM_ERROR_CORRECTION_BLOCKS[4][41]; + +}; + + + +/*---- Public exception class ----*/ + +/* + * Thrown when the supplied data does not fit any QR Code version. Ways to handle this exception include: + * - Decrease the error correction level if it was greater than Ecc::LOW. + * - If the encodeSegments() function was called with a maxVersion argument, then increase + * it if it was less than QrCode::MAX_VERSION. (This advice does not apply to the other + * factory functions because they search all versions up to QrCode::MAX_VERSION.) + * - Split the text data into better or optimal segments in order to reduce the number of bits required. + * - Change the text or binary data to be shorter. + * - Change the text to fit the character set of a particular segment mode (e.g. alphanumeric). + * - Propagate the error upward to the caller/user. + */ +class data_too_long : public std::length_error { + + public: explicit data_too_long(const std::string &msg); + +}; + + + +/* + * An appendable sequence of bits (0s and 1s). Mainly used by QrSegment. + */ +class BitBuffer final : public std::vector { + + /*---- Constructor ----*/ + + // Creates an empty bit buffer (length 0). + public: BitBuffer(); + + + + /*---- Method ----*/ + + // Appends the given number of low-order bits of the given value + // to this buffer. Requires 0 <= len <= 31 and val < 2^len. + public: void appendBits(std::uint32_t val, int len); + +}; + +} diff --git a/Widgets/CMakeLists.txt b/Widgets/CMakeLists.txt new file mode 100644 index 0000000..a2fefb4 --- /dev/null +++ b/Widgets/CMakeLists.txt @@ -0,0 +1,63 @@ +# Copyright (C) 2024 Sauntor + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +################################## +# Resuable widgets for Qili # +################################## + +set(WIDGETS_TS_FILES QiliWidgets_zh_CN.ts) +set(WIDGETS_RC_FILES QiliWidgets.qrc) +set(WIDGETS_SRC_FILES + WidgetsGlobal.h + + QiliDialog.h + QiliDialog.cpp + + QiliTextField.h + QiliTextField.cpp + + QiliTitleBar.h + QiliTitleBar.cpp + QiliTitleBar.ui +) + +qili_add_library(QiliWidgets) +target_compile_definitions(QiliWidgets PRIVATE QILI_WIDGETS_LIBRARY) +target_include_directories(QiliWidgets PRIVATE ${CMAKE_BINARY_DIR}) +target_sources(QiliWidgets PRIVATE + ${WIDGETS_SRC_FILES} + ${WIDGETS_TS_FILES} + ${WIDGETS_RC_FILES} +) +target_link_libraries(QiliWidgets PRIVATE Qt::Core Qt::Widgets) + +if (QT_VERSION_MAJOR EQUAL 5) + qt5_create_translation(WIDGETS_QM_FILES ${WIDGETS_SRC_FILES} ${WIDGETS_TS_FILES}) + add_custom_target(QiliWidgets_lrelease ALL DEPENDS ${WIDGETS_QM_FILES}) +else() + qt_add_translations(QiliWidgets TS_FILES ${WIDGETS_TS_FILES} QM_FILES_OUTPUT_VARIABLE WIDGETS_QM_FILES) +endif() + + +install(TARGETS QiliWidgets + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT Runtime +) +install(FILES ${WIDGETS_QM_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/translations) + +qili_finalize_target(QiliWidgets) diff --git a/Widgets/QiliDialog.cpp b/Widgets/QiliDialog.cpp new file mode 100644 index 0000000..f1a1103 --- /dev/null +++ b/Widgets/QiliDialog.cpp @@ -0,0 +1,112 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliDialog.h" + +#include +#include + +QiliDialog::QiliDialog(QWidget *parent, Qt::WindowFlags f) + :QDialog(parent, f) +{} + +QiliDialog::~QiliDialog() +{} + +void QiliDialog::mousePressEvent(QMouseEvent *event) +{ + QDialog::mousePressEvent(event); + if (event->button() != Qt::LeftButton) { + return; + } + +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + auto child = childAt(event->scenePosition().toPoint()); +#else + auto child = childAt(event->localPos().toPoint()); +#endif + bool should = child == nullptr; + if (!should) { + auto meta = child->metaObject(); +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + should = std::strcmp(meta->className(), "QWidget") == 0 + || std::strcmp(meta->className(), "QiliTitleBar") == 0; +#else + should = strcmp(meta->className(), "QWidget") == 0 + || strcmp(meta->className(), "QiliTitleBar") == 0; +#endif + } + if (!should) { + auto *parent = child->parentWidget(); + should = parent != nullptr && parent->property("qili-widget").toString() == "titlebar"; + } + + if (should) { +#if QILI_ON_WIN32 + // no need to change cursor +#else + mSavedCursor = cursor(); + setCursor(Qt::SizeAllCursor); +#endif + +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + mStart = event->globalPosition(); +#else + mStart = event->globalPos(); +#endif + mPos = pos(); + qDebug() << "Move Start: @" << mPos; + } else { + qDebug() << "Found Child: " << child->metaObject()->className() << " => " << child->objectName(); + } +} + +void QiliDialog::mouseReleaseEvent(QMouseEvent *event) +{ + QDialog::mouseReleaseEvent(event); + if (event->button() != Qt::LeftButton) { + return; + } + mStart = QPointF(0, 0); + qDebug() << "Move Finised: " << mPos; +#if QILI_ON_WIN32 + // nothing need to do +#else + setCursor(mSavedCursor); +#endif +} + +void QiliDialog::mouseMoveEvent(QMouseEvent *event) +{ + QDialog::mouseMoveEvent(event); + qDebug() << "Move Moving: @" << mStart; + if (!mStart.isNull()) { +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + auto current = event->globalPosition(); +#else + auto current = event->globalPos(); +#endif + qDebug() << "Move Moving: start = " << mStart << ", pos = " << mPos << ", current = " << current; + move(mPos.x() + current.x() - mStart.x(), mPos.y() + current.y() - mStart.y()); + } +} + +void QiliDialog::show() +{ + QDialog::show(); + activateWindow(); + raise(); +} diff --git a/Widgets/QiliDialog.h b/Widgets/QiliDialog.h new file mode 100644 index 0000000..0d98a11 --- /dev/null +++ b/Widgets/QiliDialog.h @@ -0,0 +1,49 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILIDIALOG_H +#define QILIDIALOG_H + +#include "Config.h" +#include "WidgetsGlobal.h" + +#include +#include +#include + +class QILI_WIDGETS_EXPORT QiliDialog : public QDialog +{ +public: + QiliDialog(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::FramelessWindowHint); + ~QiliDialog(); + +public slots: + void show(); + +protected: + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void mouseReleaseEvent(QMouseEvent *event) override; + virtual void mouseMoveEvent(QMouseEvent *event) override; + +private: + QPointF mStart{0, 0}; + QPoint mPos{0, 0}; +#if !QILI_ON_WIN32 + QCursor mSavedCursor; +#endif +}; + +#endif // QILIDIALOG_H diff --git a/Widgets/QiliTextField.cpp b/Widgets/QiliTextField.cpp new file mode 100644 index 0000000..907ba37 --- /dev/null +++ b/Widgets/QiliTextField.cpp @@ -0,0 +1,129 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliTextField.h" + +#include + +QiliTextField::QiliTextField(QWidget *parent) + : QWidget{parent} +{ + this->setProperty("qili-widget", "text-field"); + + mText = new QLineEdit(this); + mText->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + + mLabel = new QLabel("Field", this); + + mClear = new QPushButton(this); + mClear->setFlat(true); + mClear->setGeometry(0, 0, 16, 16); + mClear->setProperty("qili-btn", "clear"); + + mLayout = new QHBoxLayout(); + mLayout->addWidget(mLabel, 3); + mLayout->addWidget(mText, 8); + mLayout->setSpacing(0); + mLayout->setContentsMargins(0, 0, 0, 0); + + this->setLayout(mLayout); + this->setMinimumHeight(31); + this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + this->setGeometry(0, 0, 200, 31); + this->setContentsMargins(0, 0, 0, 0); + + QObject::connect(mClear, &QPushButton::clicked, this, &QiliTextField::onClearClicked); + QObject::connect(mText, &QLineEdit::textChanged, this, &QiliTextField::onTextChanged); +} + +void QiliTextField::setLabel(const QString &label) +{ + mLabel->setText(label); +} + +QString QiliTextField::label() const +{ + return mLabel->text(); +} + +void QiliTextField::setText(const QString &text) +{ + mText->setText(text); +} + +QString QiliTextField::text() const +{ + return mText->text(); +} + +void QiliTextField::setEchoMode(QLineEdit::EchoMode value) +{ + mText->setEchoMode(value); +} + +QLineEdit::EchoMode QiliTextField::echoMode() const +{ + return mText->echoMode(); +} + +void QiliTextField::setPlaceholder(const QString &text) +{ + mText->setPlaceholderText(text); +} + +QString QiliTextField::placeholder() const +{ + return mText->placeholderText(); +} + +void QiliTextField::setAlignment(Qt::Alignment alignment) +{ + mLabel->setAlignment(alignment); +} + +Qt::Alignment QiliTextField::alignment() const +{ + return mLabel->alignment(); +} + +void QiliTextField::clear() +{ + mText->clear(); +} + +void QiliTextField::resizeEvent(QResizeEvent *event) +{ + auto width = mText->height() / 11.0; + auto height = width; + auto x = this->width() - width * 9; + auto y = this->height() - height * 9; + mClear->setGeometry(qRound(x), qRound(y), qRound(width * 7), qRound(height * 7)); + auto margins = mText->textMargins(); + margins.setRight(qRound(width * 9)); + mText->setTextMargins(margins); + QWidget::resizeEvent(event); +} + +void QiliTextField::onClearClicked() +{ + mText->clear(); +} + +void QiliTextField::onTextChanged(const QString &text) +{ + emit textChanged(text); +} diff --git a/Widgets/QiliTextField.h b/Widgets/QiliTextField.h new file mode 100644 index 0000000..db4f2d8 --- /dev/null +++ b/Widgets/QiliTextField.h @@ -0,0 +1,77 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILITEXTFIELD_H +#define QILITEXTFIELD_H + +#include "WidgetsGlobal.h" + +#include +#include +#include +#include +#include +#include + +class QILI_WIDGETS_EXPORT QiliTextField : public QWidget +{ + Q_OBJECT + Q_PROPERTY(QLineEdit::EchoMode echoMode READ echoMode WRITE setEchoMode FINAL) + Q_PROPERTY(QString label READ label WRITE setLabel FINAL) + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged FINAL) + Q_PROPERTY(QString placeholder READ placeholder WRITE setPlaceholder FINAL) + Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment FINAL) + +public: + explicit QiliTextField(QWidget *parent = nullptr); + + void setLabel(const QString &label); + QString label() const; + + void setText(const QString &text); + QString text() const; + + void setEchoMode(QLineEdit::EchoMode value); + QLineEdit::EchoMode echoMode() const; + + void setPlaceholder(const QString &text); + QString placeholder() const; + + void setAlignment(Qt::Alignment alignment); + Qt::Alignment alignment() const; + +signals: + void textChanged(QString text); + +public slots: + void clear(); + +protected: + void resizeEvent(QResizeEvent *) override; + +private slots: + void onClearClicked(); + void onTextChanged(const QString &text); + +private: + QLineEdit *mText; + QLineEdit::EchoMode mEchoMode = QLineEdit::Normal; + QLabel *mLabel; + QPushButton *mClear; + QHBoxLayout *mLayout; +}; + +#endif // QILITEXTFIELD_H diff --git a/Widgets/QiliTitleBar.cpp b/Widgets/QiliTitleBar.cpp new file mode 100644 index 0000000..8f18346 --- /dev/null +++ b/Widgets/QiliTitleBar.cpp @@ -0,0 +1,50 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "QiliTitleBar.h" +#include "ui_QiliTitleBar.h" + +QiliTitleBar::QiliTitleBar(QWidget *parent) + : QWidget(parent) + , ui(new Ui::QiliTitleBar) +{ + ui->setupUi(this); + ui->titleLabel->setText(QWidget::windowTitle()); + QObject::connect(ui->closeButton, &QPushButton::clicked, this, &QiliTitleBar::onCloseClicked); +} + +QiliTitleBar::~QiliTitleBar() +{ + delete ui; +} + +QString QiliTitleBar::text() const +{ + return ui->titleLabel->text(); +} + +void QiliTitleBar::setText(const QString &text) +{ + if (text != ui->titleLabel->text()) { + ui->titleLabel->setText(text); + emit textChanged(text); + } +} + +void QiliTitleBar::onCloseClicked() +{ + emit closing(); +} diff --git a/Widgets/QiliTitleBar.h b/Widgets/QiliTitleBar.h new file mode 100644 index 0000000..9ef026b --- /dev/null +++ b/Widgets/QiliTitleBar.h @@ -0,0 +1,53 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef QILITITLEBAR_H +#define QILITITLEBAR_H + +#include "WidgetsGlobal.h" + +#include + +namespace Ui { +class QiliTitleBar; +} + +class QILI_WIDGETS_EXPORT QiliTitleBar : public QWidget +{ + Q_OBJECT + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged FINAL) + +public: + explicit QiliTitleBar(QWidget *parent = nullptr); + ~QiliTitleBar(); + + QString text() const; + +signals: + void closing(); + void textChanged(const QString &text); + +public slots: + void setText(const QString &text); + +private slots: + void onCloseClicked(); + +private: + Ui::QiliTitleBar *ui; +}; + +#endif // QILITITLEBAR_H diff --git a/Widgets/QiliTitleBar.ui b/Widgets/QiliTitleBar.ui new file mode 100644 index 0000000..98a8c8d --- /dev/null +++ b/Widgets/QiliTitleBar.ui @@ -0,0 +1,105 @@ + + + QiliTitleBar + + + + 0 + 0 + 400 + 28 + + + + + 0 + 0 + + + + Form + + + titlebar + + + + 0 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 16 + 16 + + + + + + + + Qili + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + + 16 + 16 + + + + + + + + 16 + 16 + + + + true + + + + + + + + diff --git a/Widgets/QiliWidgets.qrc b/Widgets/QiliWidgets.qrc new file mode 100644 index 0000000..ae962bd --- /dev/null +++ b/Widgets/QiliWidgets.qrc @@ -0,0 +1,14 @@ + + + images/save.svg + images/bili-logo.svg + images/bili-user.svg + images/clear_on.svg + images/clear.svg + images/close_on.svg + images/close.svg + images/help.svg + images/qili.png + themes/light.css + + diff --git a/Widgets/QiliWidgets_zh_CN.ts b/Widgets/QiliWidgets_zh_CN.ts new file mode 100644 index 0000000..a938ff4 --- /dev/null +++ b/Widgets/QiliWidgets_zh_CN.ts @@ -0,0 +1,19 @@ + + + + + QiliTitleBar + + + + Form + 窗口 + + + + + Qili + Qili + + + diff --git a/Widgets/WidgetsGlobal.h b/Widgets/WidgetsGlobal.h new file mode 100644 index 0000000..bde7b5f --- /dev/null +++ b/Widgets/WidgetsGlobal.h @@ -0,0 +1,26 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef WIDGETSGLOBAL_H +#define WIDGETSGLOBAL_H + +#if defined(QILI_WIDGETS_LIBRARY) +#define QILI_WIDGETS_EXPORT Q_DECL_EXPORT +#else +#define QILI_WIDGETS_EXPORT Q_DECL_IMPORT +#endif + +#endif // WIDGETSGLOBAL_H diff --git a/Widgets/images/bili-logo.svg b/Widgets/images/bili-logo.svg new file mode 100644 index 0000000..427af28 --- /dev/null +++ b/Widgets/images/bili-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Widgets/images/bili-user.svg b/Widgets/images/bili-user.svg new file mode 100644 index 0000000..1ab1b33 --- /dev/null +++ b/Widgets/images/bili-user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Widgets/images/clear.svg b/Widgets/images/clear.svg new file mode 100644 index 0000000..24a786c --- /dev/null +++ b/Widgets/images/clear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Widgets/images/clear_on.svg b/Widgets/images/clear_on.svg new file mode 100644 index 0000000..93c1925 --- /dev/null +++ b/Widgets/images/clear_on.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Widgets/images/close.svg b/Widgets/images/close.svg new file mode 100644 index 0000000..ac1761f --- /dev/null +++ b/Widgets/images/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Widgets/images/close_on.svg b/Widgets/images/close_on.svg new file mode 100644 index 0000000..aee9ef0 --- /dev/null +++ b/Widgets/images/close_on.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Widgets/images/help.svg b/Widgets/images/help.svg new file mode 100644 index 0000000..a25c310 --- /dev/null +++ b/Widgets/images/help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Widgets/images/qili.ico b/Widgets/images/qili.ico new file mode 100644 index 0000000000000000000000000000000000000000..ac1dfa915e460a633ea61c28b7adb0009bd81c83 GIT binary patch literal 19374 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 \ No newline at end of file diff --git a/Widgets/themes/light.css b/Widgets/themes/light.css new file mode 100644 index 0000000..e1af97d --- /dev/null +++ b/Widgets/themes/light.css @@ -0,0 +1,127 @@ +/* Copyright (C) 2024 Sauntor + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +QDialog { + border-width: 1px; + border-style: solid; + border-color: rgb(211, 215, 222); + border-radius:5px; + background:white; +} + +QLabel[qili-widget="titlebar"] { + font-size: 18px; + font-weight: normal; + line-height: 29px; + letter-spacing: 0px; + color: #FF6699; +} + +QWidget[qili-widget="titlebar"] > QLabel { + font-size: 18px; + font-weight: normal; + line-height: 29px; + letter-spacing: 0px; + color: #FF6699; +} + +QWidget[qili-widget="titlebar"] > QPushButton { + border: none; + image:url(":/images/close.svg"); +} + +QWidget[qili-widget="titlebar"] > QPushButton::hover { + image:url(":/images/close_on.svg"); +} + +QLabel[qili-widget="help"]{ + font-size: 12px; + font-weight: normal; + line-height: 17px; + letter-spacing: 0px; + color: #C9CCD0; +} + +QLabel[qili-widget="radio"]{ + font-size: 12px; + font-weight: normal; + line-height: 17px; + letter-spacing: 0px; + color: #61666D; +} + +QPushButton[qili-btn="primary"]{ + border: none; + font-size: 14px; + font-weight: normal; + line-height: 20px; + letter-spacing: 0px; + border-radius: 4px; + background: #FF6699; + color: #FFFFFF; +} +QPushButton[qili-btn="primary"]:hover{ + background-color:rgb(255, 79, 135); +} +QPushButton[qili-btn="primary"]:pressed{ + background-color:rgb(220, 88, 132); +} +QPushButton[qili-btn="clear"]{ + image:url(":/images/clear.svg"); +} +QPushButton[qili-btn="clear"]:hover{ + image:url(":/images/clear_on.svg"); +} + +QRadioButton[qili-btn="radio"]::indicator { + width: 15px; + height: 15px; +} +QRadioButton[qili-btn="radio"]::indicator:checked { + image: url(":/images/save.svg"); +} +QWidget[qili-widget="text-field"] { +/* border: 5px solid #E3E5E7;*/ + border: 5px solid red; + border-radius: 4px; +} + + +QWidget[qili-widget="text-field"] > QLabel { + background: #F6F7F8; + border: 1px solid #E3E5E7; + border-right: 1px solid #E3E5E7; + border-radius: 4px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + font-size: 14px; + font-weight: bold; + line-height: 16px; + color: #18191C; + padding: 0px; +} + +QWidget[qili-widget="text-field"] > QLineEdit { + border: 1px solid #E3E5E7; + border-left: 0px; + border-radius: 4px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + font-size: 14px; + font-weight: normal; + line-height: 16px; + letter-spacing: 0px; +} diff --git a/changelog b/changelog new file mode 100644 index 0000000..30227c7 --- /dev/null +++ b/changelog @@ -0,0 +1,3 @@ +- 2024-01-12 v1.0.0 ready for windows 10, ubuntu/kylin 22.04 and tumbleweed +- 2024-01-10 preview-1.0.0 support windows 10+ +- 2024-01-08 preview-1.0.0 ready for deepin 20.9 and tumbleweed