chore(CPP): Basic project configuration setup using Qt6
Considerations - Use versioned files. Versioned Qt CMake APIs are disabled to force explicit calls and say that we don't support older `QT`s - Don't use blobbing. Use `target_sources` and `qt_target_qml_sources` - Distribute `CMake` definitions closer to the context: main folders with their own `CMakeLists.txt` - Everything in libraries under `Status` namespace for cleaner code. - Includes are exposed with Module folder externally and without prefix internally - File/Folders name matches definitions they contain for uniformity that leads to cleaner code - All source files (cpp, qml, js ...) have to be added to one of the CMakeLists.txt files to be tracked by CMake build system. - Use BUILD_DEBUG, BUILD_RELEASE and BUILD_DEVELOPMENT variables from Helpers library - Avoid Include directories. Not needed anymore CMake `target_*` APIs handles this through `INTERFACE`, `PUBLIC` and `PRIVATE` scope identifiers - `StatusQ` is meant to be compiled as an external library, therefore StatusQ tests are kept inside its own directory - Forced CMake version to `3.21` for the latest features and fixes. It is desired to be kept as recent as possible due to its backward compatibility. Following Qt's shipped version might be an option - Depends on status-go changes to allow forcing of arm for apple silicon Found limitations to CMake Qt API with Qt 6.3 - Having `0` as major version when using `qt_add_qml_module` doesn't work. Qml engine reports loading the `qmldir` but won't load the plugin library and no error is reported outside that exposed types are not found. - `qt_target_qml_sources` doesn't work now, it generate a double copy error when deploying qml files in bin-directory. For now we stick with adding files using `qt_add_qml_module` central place - Need to add `OUTPUT_DIRECTORY` to `qt_add_qml_module` to use the workaround - If `MACOSX_BUNDLE` target property is set breaks importing of QML files. Disabled until fixed or workaround found - For an unknown reason application executable tries to include the `QML_ELEMENT` include files, therefore for now I include all the C++ qml elements in INTERFACE
This commit is contained in:
parent
49b592daa3
commit
1e8c851283
162
CMakeLists.txt
162
CMakeLists.txt
|
@ -1,148 +1,34 @@
|
|||
cmake_minimum_required(VERSION 3.18)
|
||||
# Provide general project configuration setup
|
||||
#
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
set(STATUS_QT_VERSION 6.3)
|
||||
set(STATUS_VERSION 0.6.0)
|
||||
|
||||
project(status-desktop
|
||||
VERSION 0.1.0
|
||||
VERSION ${STATUS_VERSION}
|
||||
LANGUAGES CXX)
|
||||
|
||||
if(PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there."
|
||||
)
|
||||
endif()
|
||||
set(PROJECT_ORGANIZATION_DOMAIN "status.im")
|
||||
set(PROJECT_ORGANIZATION_NAME "Status")
|
||||
set(PROJECT_APPLICATION_NAME "Status Desktop")
|
||||
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Debug)
|
||||
endif()
|
||||
|
||||
message(STATUS "Started CMake for ${PROJECT_NAME} v${PROJECT_VERSION}...\n")
|
||||
|
||||
include(ExternalProject)
|
||||
|
||||
# QtCreator supports the following variables for Android, which are identical to qmake Android variables.
|
||||
# Check https://doc.qt.io/qt/deployment-android.html for more information.
|
||||
# They need to be set before the find_package(...) calls below.
|
||||
|
||||
#if(ANDROID)
|
||||
# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
|
||||
# if (ANDROID_ABI STREQUAL "armeabi-v7a")
|
||||
# set(ANDROID_EXTRA_LIBS
|
||||
# ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libcrypto.so
|
||||
# ${CMAKE_CURRENT_SOURCE_DIR}/path/to/libssl.so)
|
||||
# endif()
|
||||
#endif()
|
||||
|
||||
find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Quick Widgets Concurrent REQUIRED)
|
||||
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Quick Widgets Concurrent REQUIRED)
|
||||
|
||||
set(PROJECT_SOURCES
|
||||
src-cpp/main.cpp
|
||||
src-cpp/constants.cpp
|
||||
src-cpp/logs.cpp
|
||||
resources.rcc
|
||||
)
|
||||
|
||||
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
|
||||
qt_add_executable(status-desktop
|
||||
${PROJECT_SOURCES}
|
||||
)
|
||||
else()
|
||||
if(ANDROID)
|
||||
add_library(status-desktop SHARED
|
||||
${PROJECT_SOURCES}
|
||||
)
|
||||
else()
|
||||
add_executable(status-desktop
|
||||
${PROJECT_SOURCES}
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/vendor/status-go bin/status-go)
|
||||
|
||||
# The following are dependencies that do not include CMakeList.txt files
|
||||
# Create a PR in those projects so we can include them just like it was
|
||||
# done with status-go
|
||||
include(cmake/QRCodeGen.cmake)
|
||||
include(cmake/Keycard.cmake)
|
||||
|
||||
# Submodiles
|
||||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src-cpp/dotherside)
|
||||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src-cpp/app)
|
||||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src-cpp/app_service)
|
||||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src-cpp/backend)
|
||||
|
||||
target_include_directories(status-desktop PUBLIC include)
|
||||
|
||||
|
||||
target_compile_definitions(status-desktop
|
||||
PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>)
|
||||
|
||||
target_link_libraries(status-desktop
|
||||
PRIVATE
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::Quick
|
||||
Qt${QT_VERSION_MAJOR}::Widgets
|
||||
DOtherSide
|
||||
statusgo_shared # <- Link status-go with this target (remove `_shared` for static linking)
|
||||
qrcodegen
|
||||
keycard
|
||||
app_service
|
||||
app
|
||||
)
|
||||
|
||||
|
||||
add_custom_target(rcc ALL DEPENDS resources.rcc)
|
||||
add_custom_command(
|
||||
OUTPUT resources.rcc
|
||||
COMMENT "Building resources.rcc"
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -f ${CMAKE_CURRENT_SOURCE_DIR}/resources.rcc
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -f ${CMAKE_CURRENT_SOURCE_DIR}/ui/resources.qrc
|
||||
COMMAND go run
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ui/generate-rcc.go
|
||||
-source=${CMAKE_CURRENT_SOURCE_DIR}/ui
|
||||
-output=${CMAKE_CURRENT_SOURCE_DIR}/ui/resources.qrc
|
||||
COMMAND rcc
|
||||
--binary
|
||||
$<$<CONFIG:Debug>:--no-compress>
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ui/resources.qrc
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/resources/resources.qrc
|
||||
-o ${CMAKE_CURRENT_SOURCE_DIR}/resources.rcc
|
||||
VERBATIM
|
||||
USES_TERMINAL
|
||||
)
|
||||
|
||||
|
||||
# run
|
||||
add_custom_target(run
|
||||
COMMENT "Execute desktop"
|
||||
DEPENDS status-desktop
|
||||
COMMAND ./status-desktop
|
||||
VERBATIM
|
||||
USES_TERMINAL
|
||||
)
|
||||
|
||||
|
||||
# run debug
|
||||
add_custom_target(run-debug
|
||||
COMMENT "Execute desktop with debugger on port 1234"
|
||||
DEPENDS status-desktop
|
||||
COMMAND ./status-desktop -qmljsdebugger=port:1234
|
||||
VERBATIM
|
||||
USES_TERMINAL
|
||||
)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
|
||||
include(CTest)
|
||||
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND BUILD_TESTING)
|
||||
add_subdirectory(test-cpp)
|
||||
enable_testing()
|
||||
|
||||
if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm64" AND APPLE)
|
||||
set(STATUSGO_FORCE_ARCH ${CMAKE_HOST_SYSTEM_PROCESSOR})
|
||||
endif()
|
||||
|
||||
# status-desktop application
|
||||
add_subdirectory(vendor)
|
||||
add_subdirectory(libs)
|
||||
add_subdirectory(app)
|
||||
add_subdirectory(test)
|
||||
# TODO: temporary not to duplicate resources until we switch to c++ app then it can be refactored
|
||||
add_subdirectory(ui/imports/assets)
|
||||
|
|
66
README.md
66
README.md
|
@ -5,69 +5,3 @@ Desktop client for the [Status Network](https://statusnetwork.com/) built with [
|
|||
![https://github.com/status-im/nim-status-client/blob/master/screenshot.png](https://github.com/status-im/nim-status-client/blob/master/screenshot.png)
|
||||
|
||||
Dev Docs: [https://hackmd.io/@status-desktop/B1naRjxh_/https%3A%2F%2Fhackmd.io%2F%40status-desktop%2FB1eOaf-nd](https://hackmd.io/@status-desktop/B1naRjxh_/https%3A%2F%2Fhackmd.io%2F%40status-desktop%2FB1eOaf-nd)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# CPP App
|
||||
|
||||
### Setup `Linux`:
|
||||
1. conancenter
|
||||
Execute `conan remote list`. It should return this line among the results.
|
||||
```
|
||||
conancenter: https://center.conan.io [Verify SSL: True]
|
||||
```
|
||||
If it doesnt, consider upgrading conan with `pip install conan --upgrade` and then executing. `conan remote add -i 0 conancenter https://center.conan.io` . See [conan's documentation](https://docs.conan.io/en/latest/uploading_packages/remotes.html#conancenter) for more info.
|
||||
|
||||
|
||||
2. conan libstdc++11
|
||||
This applies to linux: the default conan profile does not work, since GCC uses the new C++ ABI since version 5.1 and conan, for compatibility purposes uses the old C++ ABI.
|
||||
Execute this to update the profile:
|
||||
```
|
||||
conan profile update settings.compiler.libcxx=libstdc++11 default
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
|
||||
```
|
||||
cd build
|
||||
conan install .. -s build_type=Release --build=missing
|
||||
```
|
||||
|
||||
### Setup `OS X`:
|
||||
|
||||
1. Create `conan` profile `~/.conan/profiles/clang`:
|
||||
```
|
||||
[settings]
|
||||
compiler=apple-clang
|
||||
compiler.version=12.0
|
||||
compiler.libcxx=libc++
|
||||
arch=x86_64
|
||||
os=Macos
|
||||
build_type=Release
|
||||
|
||||
[env]
|
||||
CC=/usr/bin/clang
|
||||
CXX=/usr/bin/clang++
|
||||
```
|
||||
|
||||
2. Install dependecies:
|
||||
|
||||
```
|
||||
cd build
|
||||
conan install .. --profile=clang --build=missing
|
||||
```
|
||||
|
||||
### Buid & test & run:
|
||||
```
|
||||
conan build ..
|
||||
ctest -VV -C Release
|
||||
./status-desktop
|
||||
```
|
||||
|
||||
Instead of `conan build ..` CMake may be used:
|
||||
```
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
|
||||
cmake --build . --config Release
|
||||
```
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
|
||||
|
||||
set(CMAKE_AUTORCC On)
|
||||
|
||||
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick REQUIRED)
|
||||
qt6_standard_project_setup()
|
||||
|
||||
qt6_add_executable(${PROJECT_NAME} "")
|
||||
|
||||
# TODO: Fix temporarly workaround until we make qt6_target_qml_sources work
|
||||
# Adds qml files as /Status/Application/qml/.../<file name>
|
||||
qt6_add_qml_module(${PROJECT_NAME}
|
||||
URI Status.Application
|
||||
VERSION 1.0
|
||||
QML_FILES
|
||||
qml/main.qml
|
||||
qml/Status/Application/StatusWindow.qml
|
||||
qml/Status/Application/StatusContentView.qml
|
||||
qml/Status/Application/MainShortcuts.qml
|
||||
|
||||
qml/Status/Application/MainView/MainView.qml
|
||||
|
||||
qml/Status/Application/System/StatusTrayIcon.qml
|
||||
|
||||
qml/Status/Application/Decorators/SplashScreen.qml
|
||||
OUTPUT_DIRECTORY
|
||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status/Application
|
||||
)
|
||||
|
||||
# Compile time definitions required by the project
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE BUILD_PROJECT_NAME=${PROJECT_NAME})
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE BUILD_PROJECT_ORGANIZATION_DOMAIN=${PROJECT_ORGANIZATION_DOMAIN})
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE BUILD_PROJECT_ORGANIZATION_NAME=${PROJECT_ORGANIZATION_NAME})
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE BUILD_PROJECT_APPLICATION_NAME=${PROJECT_APPLICATION_NAME})
|
||||
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE BUILD_BINARY_DIR=${CMAKE_BINARY_DIR})
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE BUILD_SOURCE_DIR=${CMAKE_SOURCE_DIR})
|
||||
|
||||
add_subdirectory(qml)
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(res)
|
||||
|
||||
include(${CMAKE_SOURCE_DIR}/cmake/platform_specific.cmake)
|
||||
string(TOLOWER ${PROJECT_ORGANIZATION_NAME} URL_ORGANIZATION_NAME)
|
||||
configure_app_os_specific(${PROJECT_NAME} ${URL_ORGANIZATION_NAME} ${PROJECT_ORGANIZATION_DOMAIN} ${PROJECT_VERSION_MAJOR} ${PROJECT_VERSION_MINOR} ${PROJECT_VERSION_PATCH})
|
||||
|
||||
target_compile_definitions(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
$<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>
|
||||
)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
Qt6::Quick
|
||||
|
||||
# TODO: Use Status:: namespace
|
||||
#Core
|
||||
Helpers
|
||||
Onboarding
|
||||
Assets
|
||||
)
|
||||
|
||||
# QtCreator needs this
|
||||
set(QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/qml;${QML_IMPORT_PATH} CACHE STRING "For QtCreator" FORCE)
|
||||
list(REMOVE_DUPLICATES QML_IMPORT_PATH)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# CPP App
|
||||
|
||||
## Setup dependencies
|
||||
|
||||
### 1. conancenter
|
||||
|
||||
Execute `conan remote list`. It should return this line among the results:
|
||||
|
||||
```bash
|
||||
conancenter: https://center.conan.io [Verify SSL: True]
|
||||
```
|
||||
|
||||
If it doesn't, consider upgrading conan with `pip install conan --upgrade` and then executing. `conan remote add -i 0 conancenter https://center.conan.io`. See [conan's documentation](https://docs.conan.io/en/latest/uploading_packages/remotes.html#conancenter) for more info.
|
||||
|
||||
### 2. conan libstdc++11
|
||||
|
||||
This applies to linux: the default conan profile does not work, since GCC uses the new C++ ABI since version 5.1 and conan, for compatibility purposes uses the old C++ ABI.
|
||||
Execute this to update the profile:
|
||||
|
||||
```bash
|
||||
conan profile update settings.compiler.libcxx=libstdc++11 default
|
||||
```
|
||||
|
||||
### 2. Install dependencies
|
||||
|
||||
```bash
|
||||
conan install . --profile=<Platform specific conan profile> -s build_type=Release --build=missing -if=build/conan
|
||||
```
|
||||
|
||||
Platform specific conan profile
|
||||
|
||||
- Macos:
|
||||
- Intel: `vendor/conan-configs/apple-arm64.ini`
|
||||
- Apple silicon: `vendor/conan-configs/apple-x86_64.ini`
|
||||
- Windows: TODO
|
||||
- Linux: TODO
|
||||
|
||||
## Buid, test & run
|
||||
|
||||
Platform specific Qt prefix path
|
||||
|
||||
- Macos: `$HOME/Qt/6.3.0/macos`
|
||||
- Windows: TODO
|
||||
- Linux: TODO
|
||||
|
||||
### Build with conan
|
||||
|
||||
```bash
|
||||
CMAKE_PREFIX_PATH=<Qt prefix path> conan build . -if=build/conan -bf=build
|
||||
ctest -VV -C Release
|
||||
./status-desktop
|
||||
```
|
||||
|
||||
### Build with cmake
|
||||
|
||||
```bash
|
||||
cmake -B build -S . -DCMAKE_PREFIX_PATH=<Qt prefix path> -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=build/conan/conan_toolchain.cmake
|
||||
cmake --build build --config Release
|
||||
```
|
|
@ -0,0 +1,15 @@
|
|||
# Note that the automatic plugin generation is only possible if the module does not do anything besides registering the types.
|
||||
# If it needs to do something more advanced like registering an image provider in initializeEngine, you still need to manually write the plugin. qt6_add_qml_module has support for this with NO_GENERATE_PLUGIN_SOURCE.
|
||||
|
||||
target_sources(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt
|
||||
)
|
||||
|
||||
add_subdirectory(Status/Application)
|
||||
|
||||
# Qt 6.3 fails calling qt6_target_qml_sources
|
||||
#qt6_target_qml_sources(${PROJECT_NAME}
|
||||
# QML_FILES
|
||||
# main.qml
|
||||
#)
|
|
@ -0,0 +1,12 @@
|
|||
target_sources(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt
|
||||
)
|
||||
|
||||
# Qt 6.3 fails calling qt6_target_qml_sources
|
||||
#qt6_target_qml_sources(${PROJECT_NAME}
|
||||
# QML_FILES
|
||||
# StatusWindow.qml
|
||||
# StatusContentView.qml
|
||||
# TODO
|
||||
#)
|
|
@ -0,0 +1,37 @@
|
|||
import QtQuick
|
||||
|
||||
import Status.Assets
|
||||
import Status.Core.Theme
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
signal animationFinished()
|
||||
function show() {
|
||||
splashLogo.playing = true
|
||||
}
|
||||
|
||||
implicitWidth: splashLogo.implicitWidth
|
||||
implicitHeight: splashLogo.implicitHeight
|
||||
|
||||
visible: (opacity > 0.0001)
|
||||
|
||||
// TODO: consider bringing POC attempt to use lottie animations
|
||||
AnimatedImage {
|
||||
id: splashLogo
|
||||
anchors.centerIn: parent
|
||||
scale: 0.5
|
||||
source: Resources.gif("status_splash_" + Style.theme.name)
|
||||
|
||||
readonly property real frameToStartAnimation: frameCount/2
|
||||
readonly property real animationRange: (frameCount - frameToStartAnimation)
|
||||
|
||||
onCurrentFrameChanged: {
|
||||
if(currentFrame > frameToStartAnimation)
|
||||
root.opacity = 1 - (currentFrame - frameToStartAnimation)/animationRange
|
||||
if(currentFrame === (frameCount - 1))
|
||||
root.animationFinished()
|
||||
}
|
||||
playing: false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import QtQuick
|
||||
|
||||
/*!
|
||||
*/
|
||||
Item {
|
||||
required property Window window;
|
||||
property alias enableHideWindow: hideWindowShortcut.enabled
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.FullScreen
|
||||
onActivated: {
|
||||
if (visibility === Window.FullScreen)
|
||||
window.showNormal()
|
||||
else
|
||||
window.showFullScreen()
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+M"
|
||||
onActivated: {
|
||||
if (visibility === Window.Minimized)
|
||||
window.showNormal()
|
||||
else
|
||||
window.showMinimized()
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
id: hideWindowShortcut
|
||||
sequences: [StandardKey.Close]
|
||||
onActivated: window.visible = false;
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Quit
|
||||
onActivated: Qt.quit()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
/// Responsible for setup of user workflows after onboarding
|
||||
Item {
|
||||
id: root
|
||||
|
||||
/// Emited when everything is loaded and UX ready
|
||||
signal ready()
|
||||
|
||||
Component.onCompleted: root.ready()
|
||||
|
||||
implicitWidth: mainLayout.implicitWidth
|
||||
implicitHeight: mainLayout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
RowLayout {}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "TODO MainView"
|
||||
}
|
||||
Button {
|
||||
text: "Quit"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: Qt.quit()
|
||||
}
|
||||
|
||||
RowLayout {}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import Status.Application
|
||||
import Status.Onboarding
|
||||
|
||||
/// Has entry responsibility for the main workflows
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: d.isViewLoaded ? d.loadedView.implicitWidth : 800
|
||||
implicitHeight: d.isViewLoaded ? d.loadedView.implicitHeight : 600
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
readonly property bool isViewLoaded: contentLoader.status === Loader.Ready
|
||||
readonly property Item loadedView: isViewLoaded ? contentLoader.item : null
|
||||
}
|
||||
|
||||
Component {
|
||||
id: onboardingViewComponent
|
||||
|
||||
OnboardingView {
|
||||
onUserLoggedIn: {
|
||||
splashScreenPopup.open()
|
||||
contentLoader.sourceComponent = mainViewComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mainViewComponent
|
||||
|
||||
MainView {
|
||||
onReady: splashScreenPopup.close()
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: splashScreenPopup
|
||||
|
||||
onAboutToShow: splashScreenLoader.active = true
|
||||
onClosed: splashScreenLoader.active = false
|
||||
anchors.centerIn: Overlay.overlay
|
||||
|
||||
Loader {
|
||||
id: splashScreenLoader
|
||||
active: false
|
||||
sourceComponent: SplashScreen {
|
||||
id: splasScreen
|
||||
onAnimationFinished: splashScreenPopup.close()
|
||||
}
|
||||
onStatusChanged: if(status === Loader.Ready) item.show()
|
||||
}
|
||||
background: Item {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
|
||||
anchors.fill: parent
|
||||
sourceComponent: onboardingViewComponent
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Qt.labs.settings
|
||||
|
||||
import Status.Application
|
||||
|
||||
/** Administrative scope
|
||||
*/
|
||||
Window {
|
||||
id: root
|
||||
|
||||
minimumWidth: 900
|
||||
minimumHeight: 600
|
||||
|
||||
Component.onCompleted: {
|
||||
width: mainLayout.implicitWidth
|
||||
height: mainLayout.implicitHeight
|
||||
}
|
||||
|
||||
visible: true
|
||||
title: qsTr(Qt.application.name)
|
||||
|
||||
flags: Qt.FramelessWindowHint
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
// TODO: nav-bar?
|
||||
// StatusAppNavBar {
|
||||
// }
|
||||
|
||||
StatusContentView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationController {
|
||||
id: appController
|
||||
}
|
||||
|
||||
Settings {
|
||||
property alias x: root.x
|
||||
property alias y: root.y
|
||||
property alias width: root.width
|
||||
property alias height: root.height
|
||||
|
||||
// TODO: set this in non-deployment to the development "Status" folder
|
||||
//fileName: `${appController.userSettings.directory}/appController.userSettings.fileName`
|
||||
}
|
||||
|
||||
MainShortcuts {
|
||||
window: root
|
||||
enableHideWindow: true // TODO: Only if browser selected
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import Qt.labs.platform
|
||||
|
||||
import Status.Assets
|
||||
|
||||
SystemTrayIcon {
|
||||
id: root
|
||||
|
||||
property bool production: true
|
||||
|
||||
signal showApplication()
|
||||
|
||||
visible: true
|
||||
icon.source: {
|
||||
if (production)
|
||||
return Qt.platform.os === "osx" ? Resources.svg("status-logo-icon") : Resources.png("status-logo")
|
||||
else
|
||||
return Resources.svg("status-logo-dark")
|
||||
}
|
||||
icon.mask: false
|
||||
|
||||
menu: Menu {
|
||||
MenuItem {
|
||||
text: qsTr("Open Status")
|
||||
onTriggered: root.showApplication()
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: qsTr("Quit")
|
||||
onTriggered: Qt.quit()
|
||||
}
|
||||
}
|
||||
|
||||
onActivated: function (reason) {
|
||||
if (reason !== SystemTrayIcon.Context && Qt.platform.os !== "osx")
|
||||
root.showApplication()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import QtQuick
|
||||
|
||||
import Status.Application
|
||||
|
||||
/**
|
||||
QML entry point with minimal responsibilities
|
||||
*/
|
||||
StatusWindow {
|
||||
id: root
|
||||
|
||||
StatusTrayIcon {
|
||||
onShowApplication: {
|
||||
root.show()
|
||||
root.raise()
|
||||
root.requestActivate()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
# TODO workaround until Qt6 API is clarified
|
||||
target_sources(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/app.qrc
|
||||
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt
|
||||
)
|
||||
|
||||
# TODO find out why it isn't working
|
||||
#
|
||||
#qt6_target_qml_sources(${PROJECT_NAME}
|
||||
# RESOURCES
|
||||
# qtquickcontrols2.conf
|
||||
# PREFIX ""
|
||||
#)
|
|
@ -0,0 +1,5 @@
|
|||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>qtquickcontrols2.conf</file>
|
||||
</qresource>
|
||||
</RCC>
|
|
@ -0,0 +1,2 @@
|
|||
[Controls]
|
||||
Style=Universal
|
|
@ -0,0 +1,7 @@
|
|||
#include "ApplicationController.h"
|
||||
|
||||
ApplicationController::ApplicationController(QObject *parent)
|
||||
: QObject{parent}
|
||||
{
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
|
||||
#include <QObject>
|
||||
#include <QtQml/qqmlregistration.h>
|
||||
|
||||
/**
|
||||
* @brief Responsible for providing general information and utility components
|
||||
*/
|
||||
class ApplicationController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
public:
|
||||
explicit ApplicationController(QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
# Required by QML_ELEMENT
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
target_sources(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ApplicationController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ApplicationController.cpp
|
||||
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
target_include_directories(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
target_sources(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
|
||||
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt
|
||||
)
|
||||
|
||||
add_subdirectory(Application)
|
|
@ -0,0 +1,61 @@
|
|||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
|
||||
#include <QLocale>
|
||||
#include <QTranslator>
|
||||
|
||||
#include <Helpers/helpers.h>
|
||||
#include <Helpers/logs.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
|
||||
using namespace Status;
|
||||
|
||||
void setApplicationInformation(QGuiApplication& app);
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
//qInstallMessageHandler(Helpers::logFormatter);
|
||||
|
||||
QGuiApplication app(argc, argv);
|
||||
|
||||
setApplicationInformation(app);
|
||||
|
||||
QTranslator translator;
|
||||
const QStringList uiLanguages = QLocale::system().uiLanguages();
|
||||
for (const QString &locale : uiLanguages) {
|
||||
const QString baseName = STRINGIFY(BUILD_PROJECT_NAME) + QLocale(locale).name();
|
||||
if (translator.load(":/i18n/" + baseName)) {
|
||||
app.installTranslator(&translator);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
|
||||
const QUrl url(u"qrc:/Status/Application/qml/main.qml"_qs);
|
||||
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
|
||||
&app, [url](QObject *obj, const QUrl &objUrl) {
|
||||
if (!obj && url == objUrl)
|
||||
QCoreApplication::exit(-1);
|
||||
}, Qt::QueuedConnection);
|
||||
engine.load(url);
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
void setApplicationInformation(QGuiApplication& app) {
|
||||
#if !defined BUILD_PROJECT_ORGANIZATION_NAME
|
||||
static_assert(false, "Compile-time define missing: BUILD_PROJECT_ORGANIZATION_NAME")
|
||||
#endif
|
||||
app.setOrganizationName(STRINGIFY(BUILD_PROJECT_ORGANIZATION_NAME));
|
||||
#if !defined BUILD_PROJECT_ORGANIZATION_DOMAIN
|
||||
static_assert(false, "Compile-time define missing: BUILD_PROJECT_ORGANIZATION_DOMAIN")
|
||||
#endif
|
||||
app.setOrganizationDomain(STRINGIFY(BUILD_PROJECT_ORGANIZATION_DOMAIN));
|
||||
#if !defined BUILD_PROJECT_APPLICATION_NAME
|
||||
static_assert(false, "Compile-time define missing: BUILD_PROJECT_APPLICATION_NAME")
|
||||
#endif
|
||||
app.setApplicationName(STRINGIFY(BUILD_PROJECT_APPLICATION_NAME));
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
if(WIN32)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/platform_specific/windows.cmake)
|
||||
elseif(APPLE)
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/platform_specific/macos.cmake)
|
||||
endif()
|
|
@ -0,0 +1,11 @@
|
|||
function(configure_app_os_specific TARGET_ARG URL_ORGANIZATION_NAME DOMAIN_ARG VERSION_MAJOR VERSION_MINOR VERSION_PATCH)
|
||||
|
||||
set_target_properties(${TARGET_ARG} PROPERTIES
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER ${URL_ORGANIZATION_NAME}.${DOMAIN_ARG}
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION ${VERSION_MAJOR}.${VERSION_MINOR}
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_MINOR}.${VERSION_PATCH}
|
||||
# TODO: This breaks debugging QML imports
|
||||
#MACOSX_BUNDLE TRUE
|
||||
)
|
||||
|
||||
endfunction()
|
|
@ -0,0 +1,7 @@
|
|||
function(configure_app_os_specific)
|
||||
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||
WIN32_EXECUTABLE TRUE
|
||||
)
|
||||
|
||||
endfunction()
|
|
@ -0,0 +1,39 @@
|
|||
# Global not theme dependent assets
|
||||
# TODO: refactor it when switching to C++ code into Assets resource library linked or embed with the app
|
||||
#
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(Assets
|
||||
VERSION 0.1.0
|
||||
LANGUAGES CXX)
|
||||
|
||||
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
|
||||
|
||||
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Qml REQUIRED)
|
||||
qt6_standard_project_setup()
|
||||
|
||||
set_source_files_properties(qml/Status/Assets/Resources.qml PROPERTIES
|
||||
QT_QML_SINGLETON_TYPE TRUE
|
||||
)
|
||||
|
||||
qt6_add_qml_module(${PROJECT_NAME}
|
||||
URI Status.Assets
|
||||
VERSION 1.0
|
||||
# TODO: temporary until we make qt_target_qml_sources work
|
||||
QML_FILES
|
||||
qml/Status/Assets/Resources.qml
|
||||
# Required to suppress "qmllint may not work" warning
|
||||
OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status/Assets/
|
||||
)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
Qt6::Qml
|
||||
|
||||
# TODO: refactor when moved to C++ code
|
||||
UiAssets
|
||||
)
|
||||
|
||||
# QtCreator needs this
|
||||
set(QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/qml;${QML_IMPORT_PATH} CACHE STRING "For QtCreator" FORCE)
|
||||
list(REMOVE_DUPLICATES QML_IMPORT_PATH)
|
|
@ -0,0 +1,17 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQml
|
||||
|
||||
QtObject {
|
||||
readonly property string assetPath: "qrc:/Status/UiAssets"
|
||||
|
||||
function svg(name) {
|
||||
return assetPath + "/icons/" + name + ".svg";
|
||||
}
|
||||
function lottie(name) {
|
||||
return assetPath + "/lottie/" + name + ".json";
|
||||
}
|
||||
function gif(name) {
|
||||
return assetPath + "/gif/" + name + ".gif";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
add_subdirectory(Helpers)
|
||||
# TODO: enable after adding content
|
||||
#add_subdirectory(Core)
|
||||
add_subdirectory(StatusQ)
|
||||
add_subdirectory(Onboarding)
|
||||
add_subdirectory(Assets)
|
|
@ -0,0 +1,32 @@
|
|||
# Base library. Expect most of the module libraries to depend on it
|
||||
#
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(Core
|
||||
VERSION 0.1.0
|
||||
LANGUAGES CXX)
|
||||
|
||||
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
|
||||
|
||||
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED)
|
||||
qt6_standard_project_setup()
|
||||
|
||||
qt6_add_qml_module(${PROJECT_NAME}
|
||||
URI Status.Core
|
||||
VERSION 1.0
|
||||
# TODO: temporary until we make qt_target_qml_sources work
|
||||
QML_FILES
|
||||
qml/Status/Core/DevTest.qml
|
||||
# Required to suppress "qmllint may not work" warning
|
||||
OUTPUT_DIRECTORY
|
||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status/Core
|
||||
)
|
||||
|
||||
add_subdirectory(qml/Status/Core)
|
||||
add_subdirectory(src)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
Qt6::Quick
|
||||
Qt6::Qml
|
||||
)
|
|
@ -0,0 +1,4 @@
|
|||
qt6_add_resources(${PROJECT_NAME} imageresources
|
||||
PREFIX "/images"
|
||||
FILES logo.png splashscreen.png
|
||||
)
|
|
@ -0,0 +1,14 @@
|
|||
# Internally we use includes directly
|
||||
# External clients have to explicitly use the module name
|
||||
add_subdirectory(Core)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Core
|
||||
# TODO: Workaround to QML_ELEMENT Qt6
|
||||
INTERFACE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Core
|
||||
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
|
@ -0,0 +1,5 @@
|
|||
target_sources(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt
|
||||
)
|
|
@ -0,0 +1,57 @@
|
|||
# Light helpers library expected to be used by all other libraries
|
||||
#
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(Helpers
|
||||
VERSION 0.1.0
|
||||
LANGUAGES CXX)
|
||||
|
||||
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
|
||||
|
||||
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED)
|
||||
qt6_standard_project_setup()
|
||||
|
||||
add_library(${PROJECT_NAME} SHARED "")
|
||||
|
||||
# Setup configuration type (Debug/Release)
|
||||
# Inspired by https://programmingrecluse.wordpress.com/2020/02/04/detect-debug-build-with-cmake/
|
||||
if(CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_CONFIGURATION_TYPES Debug Release)
|
||||
elseif(NOT CMAKE_BUILD_TYPE)
|
||||
message("Missing Build Type! Run cmake with:\n-DCMAKE_BUILD_TYPE=Debug|Release")
|
||||
endif()
|
||||
|
||||
# Use by linker only
|
||||
set_property(GLOBAL PROPERTY DEBUG_CONFIGURATIONS Debug)
|
||||
|
||||
# Setup BUILD_DEBUG and BUILD_REALEASE if single configuration builds
|
||||
if(CMAKE_BUILD_TYPE)
|
||||
string(TOUPPER "${CMAKE_BUILD_TYPE}" _upper_build_type)
|
||||
set(BUILD_${_upper_build_type} true)
|
||||
endif()
|
||||
|
||||
set(BUILD_GENERATED_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/generated)
|
||||
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/template/BuildConfiguration.h.in"
|
||||
"${BUILD_GENERATED_DIRECTORY}/Helpers/BuildConfiguration.h"
|
||||
@ONLY)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
${BUILD_GENERATED_DIRECTORY}
|
||||
PRIVATE
|
||||
${BUILD_GENERATED_DIRECTORY}/Helpers
|
||||
)
|
||||
|
||||
target_sources(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/template/BuildConfiguration.h.in
|
||||
)
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
Qt6::Quick
|
||||
Qt6::Qml
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Helpers
|
||||
)
|
||||
|
||||
add_subdirectory(Helpers)
|
|
@ -0,0 +1,7 @@
|
|||
target_sources(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/helpers.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/logs.h
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/logs.cpp
|
||||
)
|
|
@ -0,0 +1,19 @@
|
|||
#include <Helpers/BuildConfiguration.h>
|
||||
|
||||
namespace Status::Helpers {
|
||||
|
||||
constexpr bool isDebugBuild()
|
||||
{
|
||||
#if defined BUILD_DEBUG
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if !defined STRINGIFY
|
||||
#define STRINGIFY(X) STRIFY(X)
|
||||
#define STRIFY(X) #X
|
||||
#endif
|
|
@ -1,9 +1,23 @@
|
|||
#include "helpers.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QString>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <Helpers/BuildConfiguration.h>
|
||||
|
||||
namespace Status::Helpers {
|
||||
|
||||
void logFormatter(QtMsgType type, const QMessageLogContext& context, const QString& msg)
|
||||
{
|
||||
// TODO: Refactor it into development-tools app
|
||||
//if(isDebugBuild()) {
|
||||
std::cout << msg.toLocal8Bit().data() << std::endl;
|
||||
return;
|
||||
//}
|
||||
|
||||
QByteArray localMsg = msg.toLocal8Bit();
|
||||
const char* file = context.file ? context.file : "";
|
||||
QByteArray function =
|
||||
|
@ -22,5 +36,8 @@ void logFormatter(QtMsgType type, const QMessageLogContext& context, const QStri
|
|||
case QtCriticalMsg: log = "\033[0;91mCRT \033[0m%s \033[1;91m%s \033[0;33mfile=\033[94m%s:%u %s\n"; break;
|
||||
case QtFatalMsg: log = "\033[0;31m!!! \033[0m%s \033[1m%s \033[0;33mfile=\033[94m%s:%u %s\n"; break;
|
||||
}
|
||||
fprintf(stderr, log, timestamp.constData(), localMsg.constData(), file, context.line, function.constData());
|
||||
fprintf(type == QtCriticalMsg || type == QtFatalMsg ? stderr : stdout,
|
||||
log, timestamp.constData(), localMsg.constData(), file, context.line, function.constData());
|
||||
}
|
||||
|
||||
} // namespace
|
|
@ -4,4 +4,9 @@
|
|||
#include <QDebug>
|
||||
#include <QString>
|
||||
|
||||
namespace Status::Helpers {
|
||||
|
||||
/// Formats with colloring output if not a development build
|
||||
void logFormatter(QtMsgType type, const QMessageLogContext& context, const QString& msg);
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// Cross platform and environment build definition
|
||||
#if defined(_MSC_VER)
|
||||
// The microsoft compiler sets _DEBUG appropriately when targeting a debug build.
|
||||
#if _DEBUG
|
||||
#define BUILD_DEBUG 1
|
||||
#else
|
||||
#define BUILD_RELEASE 1
|
||||
#endif
|
||||
#elif defined(__apple_build_version__)
|
||||
// Xcode might set DEBUG when targeting debug, maybe.
|
||||
#if DEBUG
|
||||
#define BUILD_DEBUG 1
|
||||
#else
|
||||
#define BUILD_RELEASE 1
|
||||
#endif
|
||||
#else
|
||||
#cmakedefine BUILD_DEBUG 1
|
||||
#cmakedefine BUILD_RELEASE 1
|
||||
#endif
|
|
@ -0,0 +1,32 @@
|
|||
# Base library. Expect most of the module libraries to depend on it
|
||||
#
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(Onboarding
|
||||
VERSION 0.1.0
|
||||
LANGUAGES CXX)
|
||||
|
||||
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
|
||||
|
||||
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED)
|
||||
qt6_standard_project_setup()
|
||||
|
||||
qt6_add_qml_module(${PROJECT_NAME}
|
||||
URI Status.Onboarding
|
||||
VERSION 1.0
|
||||
# TODO: temporary until we make qt_target_qml_sources work
|
||||
QML_FILES
|
||||
qml/Status/Onboarding/OnboardingView.qml
|
||||
# Required to suppress "qmllint may not work" warning
|
||||
OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status/Onboarding/
|
||||
)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
Qt6::Quick
|
||||
Qt6::Qml
|
||||
)
|
||||
|
||||
# QtCreator needs this
|
||||
set(QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/qml;${QML_IMPORT_PATH} CACHE STRING "For QtCreator" FORCE)
|
||||
list(REMOVE_DUPLICATES QML_IMPORT_PATH)
|
|
@ -0,0 +1,30 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
signal userLoggedIn()
|
||||
|
||||
implicitWidth: mainLayout.implicitWidth
|
||||
implicitHeight: mainLayout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
RowLayout {}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "TODO OnboardingWorkflow"
|
||||
}
|
||||
Button {
|
||||
text: "Done"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: root.userLoggedIn()
|
||||
}
|
||||
RowLayout {}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
# Light helpers library expected to be used by all other libraries
|
||||
#
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(StatusGoQt
|
||||
VERSION 0.1.0
|
||||
LANGUAGES CXX)
|
||||
|
||||
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
|
||||
|
||||
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Core REQUIRED)
|
||||
qt6_standard_project_setup()
|
||||
|
||||
add_library(${PROJECT_NAME} SHARED "")
|
||||
|
||||
# Use by linker only
|
||||
set_property(GLOBAL PROPERTY DEBUG_CONFIGURATIONS Debug)
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
Qt6::Core
|
||||
|
||||
statusgo_shared
|
||||
)
|
||||
|
||||
# Copy status-go lib close to the executable
|
||||
# Temporary workaround; TODO: see a better alternative that doesn't depend on target order (dedicated dependencies dir?)
|
||||
get_target_property(STATUSGO_LIBRARY_PATH statusgo_shared IMPORTED_LOCATION)
|
||||
configure_file(${STATUSGO_LIBRARY_PATH} ${CMAKE_CURRENT_BINARY_DIR} COPYONLY)
|
|
@ -0,0 +1,14 @@
|
|||
# Internally we use includes directly
|
||||
# External clients have to explicitly use the module name
|
||||
add_subdirectory(StatusGo)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/StatusGo
|
||||
# TODO: Workaround to QML_ELEMENT Qt6
|
||||
INTERFACE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/StatusGo
|
||||
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
|
@ -0,0 +1,27 @@
|
|||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(StatusQ
|
||||
VERSION 0.1.0
|
||||
LANGUAGES CXX)
|
||||
|
||||
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
|
||||
|
||||
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED)
|
||||
qt6_standard_project_setup()
|
||||
|
||||
qt6_add_qml_module(${PROJECT_NAME}
|
||||
URI Status
|
||||
VERSION 1.0
|
||||
# TODO: temporary until we make qt_target_qml_sources work
|
||||
QML_FILES
|
||||
# Required to suppress "qmllint may not work" warning
|
||||
OUTPUT_DIRECTORY
|
||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status
|
||||
)
|
||||
|
||||
add_subdirectory(qml/Status/Core/Theme)
|
||||
add_subdirectory(tests)
|
||||
|
||||
# QtCreator needs this
|
||||
set(QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/qml;${QML_IMPORT_PATH} CACHE STRING "For QtCreator" FORCE)
|
||||
list(REMOVE_DUPLICATES QML_IMPORT_PATH)
|
|
@ -0,0 +1,28 @@
|
|||
# Theme dependent components and assets
|
||||
project(StatusQ_Core_Theme)
|
||||
|
||||
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
|
||||
|
||||
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED)
|
||||
qt6_standard_project_setup()
|
||||
|
||||
set_source_files_properties(Style.qml PROPERTIES
|
||||
QT_QML_SINGLETON_TYPE TRUE
|
||||
)
|
||||
|
||||
qt6_add_qml_module(${PROJECT_NAME}
|
||||
URI Status.Core.Theme
|
||||
VERSION 1.0
|
||||
# TODO: temporary until we make qt_target_qml_sources work
|
||||
QML_FILES
|
||||
StatusDarkPalette.qml
|
||||
StatusDarkTheme.qml
|
||||
StatusLightPalette.qml
|
||||
StatusLightTheme.qml
|
||||
StatusPalette.qml
|
||||
StatusTheme.qml
|
||||
Style.qml
|
||||
# Required to suppress "qmllint may not work" warning
|
||||
OUTPUT_DIRECTORY
|
||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status/Core/Theme
|
||||
)
|
|
@ -0,0 +1,5 @@
|
|||
import QtQuick
|
||||
|
||||
StatusPalette {
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import QtQuick
|
||||
|
||||
StatusTheme {
|
||||
readonly property string name: "dark"
|
||||
readonly property StatusPalette palette: StatusDarkPalette {}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import QtQuick
|
||||
|
||||
StatusPalette {
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import QtQuick
|
||||
|
||||
StatusTheme {
|
||||
readonly property string name: "light"
|
||||
readonly property StatusPalette palette: StatusLightPalette {}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import QtQuick
|
||||
|
||||
Item {
|
||||
required property string name
|
||||
required property StatusPalette palette
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
//import QtQuick.Controls.Universal
|
||||
|
||||
QtObject {
|
||||
property StatusTheme theme: lightTheme
|
||||
property StatusPalette palette: theme.palette
|
||||
readonly property StatusTheme lightTheme: StatusLightTheme {}
|
||||
readonly property StatusTheme darkTheme: StatusDarkTheme {}
|
||||
|
||||
property var changeTheme: function (palette, isCurrentSystemThemeDark) {
|
||||
switch (theme) {
|
||||
case Universal.Light:
|
||||
theme = lightTheme;
|
||||
break;
|
||||
case Universal.Dark:
|
||||
theme = darkTheme;
|
||||
break;
|
||||
case Universal.System:
|
||||
current = isCurrentSystemThemeDark? darkTheme : lightTheme;
|
||||
break;
|
||||
default:
|
||||
console.warning('Unknown theme. Valid themes are "light" and "dark"')
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
cmake_minimum_required(VERSION 3.5)
|
||||
|
||||
project(TestStatusQ LANGUAGES CXX)
|
||||
|
||||
enable_testing()
|
||||
|
||||
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
|
||||
|
||||
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml QuickTest REQUIRED)
|
||||
qt6_standard_project_setup()
|
||||
|
||||
qt6_add_qml_module(${PROJECT_NAME}
|
||||
URI StatusQ.TestHelpers
|
||||
VERSION 1.0
|
||||
# TODO: temporary until we make qt_target_qml_sources work
|
||||
QML_FILES
|
||||
# Required to suppress "qmllint may not work" warning
|
||||
OUTPUT_DIRECTORY
|
||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status/TestHelpers
|
||||
)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# no need to copy around qml test files for shadow builds - just set the respective define
|
||||
add_definitions(-DQUICK_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
add_test(NAME ${PROJECT_NAME} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME} -input "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
add_custom_target("Run_${PROJECT_NAME}" COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
add_dependencies("Run_${PROJECT_NAME}" ${PROJECT_NAME})
|
||||
|
||||
# TODO: move this to a test helpers library
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||
Qt6::QuickTest
|
||||
Qt6::Qml
|
||||
Qt6::Quick
|
||||
)
|
|
@ -0,0 +1,26 @@
|
|||
#include <QtQuickTest/quicktest.h>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include "TestHelpers/MonitorQtOutput.h"
|
||||
|
||||
class TestSetup : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TestSetup() {}
|
||||
|
||||
public slots:
|
||||
void qmlEngineAvailable(QQmlEngine *engine)
|
||||
{
|
||||
// TODO: Workaround until we make StatusQ a CMake library
|
||||
engine->addImportPath("../src/");
|
||||
engine->addImportPath("./qml/");
|
||||
// TODO: Alternative to not yet supported QML_ELEMENT
|
||||
qmlRegisterType<MonitorQtOutput>("StatusQ.TestHelpers", 0, 1, "MonitorQtOutput");
|
||||
}
|
||||
};
|
||||
|
||||
QUICK_TEST_MAIN_WITH_SETUP(TestControls, TestSetup)
|
||||
|
||||
#include "main.moc"
|
|
@ -0,0 +1,23 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQml
|
||||
import QtTest
|
||||
|
||||
QtObject {
|
||||
|
||||
//> Simulate key and wait for side effects
|
||||
function pressKeyAndWait(test, item, key) {
|
||||
test.keyClick(key)
|
||||
|
||||
ensureRendered(test, item)
|
||||
}
|
||||
|
||||
function ensureRendered(test, item) {
|
||||
test.verify(test.waitForRendering(item, 1000))
|
||||
}
|
||||
|
||||
function expectRendering(test, item) {
|
||||
test.verify(test.isPolishScheduled(item))
|
||||
test.verify(test.waitForRendering(item, 1000))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
# Readme
|
||||
|
||||
## Developer instructions
|
||||
|
||||
CMake
|
||||
|
||||
```sh
|
||||
cd ./tests/
|
||||
cmake -B ./build/ -S .
|
||||
cmake --build ./build/
|
||||
ctest --test-dir ./build/
|
||||
```
|
||||
|
||||
QtCreator
|
||||
|
||||
- Open the `./tests/CMakeLists.txt`
|
||||
- Choose a QT kit to run the tests
|
||||
- Set `%{sourceDir}/tests` as Working Directory for the TestStatusQ target
|
||||
- In the *Test Results* panel choose Run All Tests or just run the *TestStatusQ* target
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] TestHelpers library
|
||||
- [ ] Consolidate and integrate with https://github.com/status-im/desktop-ui-tests
|
|
@ -0,0 +1,6 @@
|
|||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
add_subdirectory(TestHelpers)
|
|
@ -0,0 +1,8 @@
|
|||
target_sources(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MonitorQtOutput.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MonitorQtOutput.cpp
|
||||
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt
|
||||
)
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
#include "MonitorQtOutput.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
std::weak_ptr<QString> MonitorQtOutput::m_qtMessageOutputForSharing;
|
||||
std::mutex MonitorQtOutput::m_mutex;
|
||||
QtMessageHandler MonitorQtOutput::m_previousHandler = nullptr;
|
||||
|
||||
|
||||
MonitorQtOutput::MonitorQtOutput()
|
||||
{
|
||||
// Ensure only one instance registers a handler
|
||||
// Warning: don't call QT's logger functions inside the critical section
|
||||
std::unique_lock<std::mutex> localLock(m_mutex);
|
||||
auto globalMsgOut = m_qtMessageOutputForSharing.lock();
|
||||
auto prev = qInstallMessageHandler(qtMessageOutput);
|
||||
if(prev != qtMessageOutput)
|
||||
m_previousHandler = prev;
|
||||
if(!globalMsgOut) {
|
||||
m_thisMessageOutput = std::make_shared<QString>();
|
||||
m_qtMessageOutputForSharing = m_thisMessageOutput;
|
||||
}
|
||||
else {
|
||||
m_thisMessageOutput = globalMsgOut;
|
||||
m_start = m_thisMessageOutput->length();
|
||||
}
|
||||
}
|
||||
|
||||
MonitorQtOutput::~MonitorQtOutput()
|
||||
{
|
||||
std::unique_lock<std::mutex> localLock(m_mutex);
|
||||
if(m_thisMessageOutput.use_count() == 1) {
|
||||
// Last instance, deregister the handler
|
||||
qInstallMessageHandler(0);
|
||||
m_thisMessageOutput.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
MonitorQtOutput::qtMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
{
|
||||
std::unique_lock<std::mutex> localLock(m_mutex);
|
||||
auto globalMsgOut = m_qtMessageOutputForSharing.lock();
|
||||
assert(globalMsgOut != nullptr);
|
||||
globalMsgOut->append(msg + '\n');
|
||||
// Also reproduce the default output
|
||||
m_previousHandler(type, context, msg);
|
||||
}
|
||||
|
||||
QString
|
||||
MonitorQtOutput::qtOuput()
|
||||
{
|
||||
std::unique_lock<std::mutex> localLock(m_mutex);
|
||||
assert(m_thisMessageOutput->length() >= m_start);
|
||||
return m_thisMessageOutput->right(m_thisMessageOutput->length() - m_start);
|
||||
}
|
||||
|
||||
void
|
||||
MonitorQtOutput::restartCapturing()
|
||||
{
|
||||
std::unique_lock<std::mutex> localLock(m_mutex);
|
||||
// Ensure the messageHandler is installed. Foun to be reset at test initializaiton
|
||||
auto prev = qInstallMessageHandler(qtMessageOutput);
|
||||
if(prev != qtMessageOutput)
|
||||
m_previousHandler = prev;
|
||||
m_start = m_thisMessageOutput->length();
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
#pragma once
|
||||
|
||||
#include <QQuickItem>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
///
|
||||
/// \brief Monitor output for tests and declaratively control message handler availability
|
||||
///
|
||||
/// The captured buffer is global and each instance has a reference to it and a start pointer
|
||||
/// from its creation or last clear call
|
||||
/// The first instance installs a QT message handler @see Qt::qInstallMessageHandler then
|
||||
/// All other instances share the global buffer until the last instance goes out of scope and deregisters
|
||||
/// from Qt's global message handler and destroyes the buffer
|
||||
///
|
||||
/// \todo Check that QML doesn't keep instance between test runs
|
||||
///
|
||||
class MonitorQtOutput : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
MonitorQtOutput();
|
||||
~MonitorQtOutput();
|
||||
|
||||
/// Return captured output from the global buffer. That is from the instantiation or last `clear()` was called
|
||||
Q_INVOKABLE QString qtOuput();
|
||||
/// Reset buffer start after the last line. qtOutput won't return anything until new output is captured
|
||||
Q_INVOKABLE void restartCapturing();
|
||||
|
||||
signals:
|
||||
|
||||
private:
|
||||
static void qtMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg);
|
||||
static QtMessageHandler m_previousHandler;
|
||||
|
||||
// Use it to keep track of qInstallMessageHandler call
|
||||
static std::weak_ptr<QString> m_qtMessageOutputForSharing;
|
||||
static std::mutex m_mutex;
|
||||
std::shared_ptr<QString> m_thisMessageOutput;
|
||||
int m_start = 0;
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
# Temporary library not to duplicate resources
|
||||
# TODO: refactor it when switching to C++ code into Assets resource library linked or embed with the app
|
||||
#
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(UiAssets
|
||||
VERSION 0.1.0
|
||||
LANGUAGES CXX)
|
||||
|
||||
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
|
||||
|
||||
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Qml REQUIRED)
|
||||
qt6_standard_project_setup()
|
||||
|
||||
qt6_add_qml_module(${PROJECT_NAME}
|
||||
URI Status.UiAssets
|
||||
VERSION 1.0
|
||||
# TODO: temporary until we make qt_target_qml_sources work
|
||||
RESOURCES
|
||||
gif/status_splash_dark.gif
|
||||
gif/status_splash_light.gif
|
||||
|
||||
icons/status-logo-icon.svg
|
||||
icons/status-logo-dark.svg
|
||||
|
||||
png/traffic_lights/close.png
|
||||
png/traffic_lights/close_pressed.png
|
||||
png/traffic_lights/maximize.png
|
||||
png/traffic_lights/maximize_pressed.png
|
||||
png/traffic_lights/minimise.png
|
||||
png/traffic_lights/minimise_pressed.png
|
||||
png/status-logo.png
|
||||
|
||||
RESOURCE_PREFIX ""
|
||||
)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
Qt6::Qml
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/status-go)
|
|
@ -0,0 +1,11 @@
|
|||
[settings]
|
||||
compiler=apple-clang
|
||||
compiler.version=13.1
|
||||
compiler.libcxx=libc++
|
||||
arch=armv8
|
||||
os=Macos
|
||||
build_type=Release
|
||||
|
||||
[env]
|
||||
CC=/usr/bin/clang
|
||||
CXX=/usr/bin/clang++
|
|
@ -0,0 +1,11 @@
|
|||
[settings]
|
||||
compiler=apple-clang
|
||||
compiler.version=13.1
|
||||
compiler.libcxx=libc++
|
||||
arch=x86_64
|
||||
os=Macos
|
||||
build_type=Release
|
||||
|
||||
[env]
|
||||
CC=/usr/bin/clang
|
||||
CXX=/usr/bin/clang++
|
|
@ -1 +1 @@
|
|||
Subproject commit 05073a96407ca40313663f7423195ce767c6d9c5
|
||||
Subproject commit 23d745fe0a61501b847af9c54479d5fa763a51d0
|
Loading…
Reference in New Issue