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 0000000..236b3c9 Binary files /dev/null and b/App/images/alipay.png differ diff --git a/App/images/qili.ico b/App/images/qili.ico new file mode 100644 index 0000000..ac1dfa9 Binary files /dev/null and b/App/images/qili.ico differ diff --git a/App/images/wechat.png b/App/images/wechat.png new file mode 100644 index 0000000..0f4de4d Binary files /dev/null and b/App/images/wechat.png differ 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 0000000..ac1dfa9 Binary files /dev/null and b/Widgets/images/qili.ico differ diff --git a/Widgets/images/qili.png b/Widgets/images/qili.png new file mode 100644 index 0000000..c54bab5 Binary files /dev/null and b/Widgets/images/qili.png differ diff --git a/Widgets/images/save.svg b/Widgets/images/save.svg new file mode 100644 index 0000000..f909fec --- /dev/null +++ b/Widgets/images/save.svg @@ -0,0 +1 @@ + \ 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