New upstream version 1.6.1+ds0
authorIOhannes m zmölnig <zmoelnig@umlautS.umlaeute.mur.at>
Sun, 17 Jul 2022 19:44:45 +0000 (21:44 +0200)
committerIOhannes m zmölnig <zmoelnig@umlautS.umlaeute.mur.at>
Sun, 17 Jul 2022 19:44:45 +0000 (21:44 +0200)
127 files changed:
.clang-format-ignore
CMakeLists.txt
build
docs/Build/Linux.md
docs/Build/Mac.md
docs/Build/Meson_build.md
docs/changelog.yml
faust-src/faust2header.cpp
faust-src/zitarevdsp.dsp
jacktrip.pro
linux/flatpak/org.jacktrip.JackTrip.Devel.yml
linux/flatpak/org.jacktrip.JackTrip.Devel.yml.j2
linux/flatpak/org.jacktrip.JackTrip.yml
macos/JackTrip.app_template/Contents/Info.plist
macos/assemble_app.sh
macos/package/JackTrip.pkgproj_template
macos/package/link.sh [deleted file]
macos/package/postinstall.sh [new file with mode: 0755]
meson.build
meson_options.txt
releases/edge/mac-manifests.json [new file with mode: 0644]
releases/edge/win-manifests.json [new file with mode: 0644]
releases/stable/mac-manifests.json [new file with mode: 0644]
releases/stable/win-manifests.json [new file with mode: 0644]
scripts/hubMode/test_hub_mode_server_and_client.sh
src/AudioInterface.cpp
src/AudioInterface.h
src/Auth.cpp
src/Compressor.cpp
src/Compressor.h
src/DataProtocol.h
src/JMess.cpp
src/JMess.h
src/JackAudioInterface.h
src/JackTrip.cpp
src/JackTrip.h
src/JackTripWorker.h
src/Limiter.cpp
src/Limiter.h
src/PacketHeader.cpp
src/PacketHeader.h
src/Regulator.cpp
src/Regulator.h
src/Reverb.cpp
src/Reverb.h
src/RingBuffer.cpp
src/RingBuffer.h
src/RtAudioInterface.cpp
src/RtAudioInterface.h
src/Settings.cpp
src/UdpDataProtocol.cpp
src/UdpDataProtocol.h
src/UdpHubListener.cpp
src/build
src/dblsqd/feed.cpp [new file with mode: 0644]
src/dblsqd/feed.h [new file with mode: 0644]
src/dblsqd/release.cpp [new file with mode: 0644]
src/dblsqd/release.h [new file with mode: 0644]
src/dblsqd/semver.cpp [new file with mode: 0644]
src/dblsqd/semver.h [new file with mode: 0644]
src/dblsqd/update_dialog.cpp [new file with mode: 0644]
src/dblsqd/update_dialog.h [new file with mode: 0644]
src/dblsqd/update_dialog.ui [new file with mode: 0644]
src/gui/Browse.qml [new file with mode: 0644]
src/gui/Connected.qml [new file with mode: 0644]
src/gui/FirstLaunch.qml [new file with mode: 0644]
src/gui/JTOriginal.png [new file with mode: 0644]
src/gui/JTVS.png [new file with mode: 0644]
src/gui/Login.qml [new file with mode: 0644]
src/gui/Poppins-Bold.ttf [new file with mode: 0644]
src/gui/Poppins-Regular.ttf [new file with mode: 0644]
src/gui/Settings.qml [new file with mode: 0644]
src/gui/Setup.qml [new file with mode: 0644]
src/gui/Studio.qml [new file with mode: 0644]
src/gui/cog.svg [new file with mode: 0644]
src/gui/ethernet.png [new file with mode: 0644]
src/gui/flags/AE.svg [new file with mode: 0644]
src/gui/flags/AU.svg [new file with mode: 0644]
src/gui/flags/BE.svg [new file with mode: 0644]
src/gui/flags/BR.svg [new file with mode: 0644]
src/gui/flags/CA.svg [new file with mode: 0644]
src/gui/flags/CH.svg [new file with mode: 0644]
src/gui/flags/DE.svg [new file with mode: 0644]
src/gui/flags/FR.svg [new file with mode: 0644]
src/gui/flags/GB.svg [new file with mode: 0644]
src/gui/flags/HK.svg [new file with mode: 0644]
src/gui/flags/ID.svg [new file with mode: 0644]
src/gui/flags/IT.svg [new file with mode: 0644]
src/gui/flags/JP.svg [new file with mode: 0644]
src/gui/flags/RO.svg [new file with mode: 0644]
src/gui/flags/SE.svg [new file with mode: 0644]
src/gui/flags/SG.svg [new file with mode: 0644]
src/gui/flags/TW.svg [new file with mode: 0644]
src/gui/flags/US.svg [new file with mode: 0644]
src/gui/flags/ZA.svg [new file with mode: 0644]
src/gui/headphones.svg [new file with mode: 0644]
src/gui/jacktrip white.png [new file with mode: 0644]
src/gui/jacktrip.png [new file with mode: 0644]
src/gui/join.svg [new file with mode: 0644]
src/gui/leave.svg [new file with mode: 0644]
src/gui/logo.svg [new file with mode: 0644]
src/gui/mic.svg [new file with mode: 0644]
src/gui/private.svg [new file with mode: 0644]
src/gui/public.svg [new file with mode: 0644]
src/gui/qjacktrip.cpp
src/gui/qjacktrip.h
src/gui/qjacktrip.qrc
src/gui/qjacktrip.ui
src/gui/qjacktrip_novs.qrc [new file with mode: 0644]
src/gui/qjacktrip_novs.ui
src/gui/virtualstudio.cpp [new file with mode: 0644]
src/gui/virtualstudio.h [new file with mode: 0644]
src/gui/vs.qml [new file with mode: 0644]
src/gui/vsQuickView.cpp [new file with mode: 0644]
src/gui/vsQuickView.h [new file with mode: 0644]
src/gui/vsServerInfo.cpp [new file with mode: 0644]
src/gui/vsServerInfo.h [new file with mode: 0644]
src/gui/wedge.svg [new file with mode: 0644]
src/gui/wedge_inactive.svg [new file with mode: 0644]
src/jacktrip_globals.h
src/main.cpp
win/CodeSignTool/CodeSignTool.sh [new file with mode: 0755]
win/CodeSignTool/conf/code_sign_tool.properties [new file with mode: 0644]
win/CodeSignTool/conf/log4j2.xml [new file with mode: 0644]
win/build_installer.bat
win/files.wxs
win/jacktrip.wxs

index 8360478adfb971e1ab6de8f719b24e7b26ca2729..933795329ad431fdb4f4c316ae36e22e67acf715 100644 (file)
@@ -1 +1,4 @@
 externals/*
+faust-src/faust2header.cpp
+src/*dsp.h
+
index 40cae871e7d62959a98dca8fdbed3cbf7b6c9c32..3fb57389eabfe6e8933075812b6e2fa1a0f681b1 100644 (file)
@@ -1,15 +1,18 @@
 cmake_minimum_required(VERSION 3.12)
 set(CMAKE_OSX_DEPLOYMENT_TARGET 10.13)
+set(CMAKE_OSX_ARCHITECTURES arm64;x86_64)
 project(QJackTrip)
 
 set(nogui FALSE)
 set(rtaudio TRUE)
 set(weakjack TRUE)
-add_compile_definitions(NO_JTVS)
+set(novs FALSE)
+add_compile_definitions(NO_UPDATER)
 add_compile_definitions(QT_OPENSOURCE)
 
 if (nogui)
   add_compile_definitions(NO_GUI)
+  set(novs TRUE)
 endif ()
 
 if (rtaudio)
@@ -21,6 +24,10 @@ if (weakjack)
   include_directories("externals/weakjack")
 endif()
 
+if (novs)
+  add_compile_definitions(NO_VS)
+endif ()
+
 if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
   set (ENV{PKG_CONFIG_PATH} "/usr/local/lib/pkgconfig")
 elseif (${CMAKE_SYSTEM_NAME} MATCHES "Windows")
@@ -43,7 +50,7 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin
   find_package(PkgConfig REQUIRED)
   pkg_check_modules(JACK REQUIRED IMPORTED_TARGET jack)
   if (weakjack)
-    # On mac, weakjack doesnt't find jack unless this is explicitly included.
+    # On mac, weakjack doesn't find jack unless this is explicitly included.
     include_directories(${JACK_INCLUDE_DIRS})
   endif ()
   if (rtaudio)
@@ -51,6 +58,7 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin
   endif ()
 endif ()
 
+set_property(SOURCE src/Regulator.h PROPERTY SKIP_AUTOGEN ON)
 # Find includes in corresponding build directories
 set(CMAKE_INCLUDE_CURRENT_DIR ON)
 # Instruct CMake to run moc automatically when needed.
@@ -63,6 +71,10 @@ set(CMAKE_AUTORCC ON)
 # Find the QtWidgets library
 if (NOT nogui)
   find_package(Qt5Widgets CONFIG REQUIRED)
+  if (NOT novs)
+    find_package(Qt5Quick CONFIG REQUIRED)
+    find_package(Qt5NetworkAuth CONFIG REQUIRED)
+  endif ()
 endif ()
 find_package(Qt5Network CONFIG REQUIRED)
 
@@ -110,8 +122,18 @@ if (NOT nogui)
     src/gui/about.cpp
     src/gui/messageDialog.cpp
     src/gui/textbuf.cpp
-    src/gui/qjacktrip.qrc
   )
+  
+  if (NOT novs)
+    set (qjacktrip_SRC ${qjacktrip_SRC}
+      src/gui/virtualstudio.cpp
+      src/gui/vsServerInfo.cpp
+      src/gui/vsQuickView.cpp
+      src/gui/qjacktrip.qrc
+    )
+  else ()
+    set (qjacktrip_SRC ${qjacktrip_SRC} src/gui/qjacktrip_novs.qrc)
+  endif ()
 
   if (${CMAKE_SYSTEM_NAME} MATCHES "Windows")
     set (qjacktrip_SRC ${qjacktrip_SRC} win/qjacktrip.rc)
@@ -128,9 +150,12 @@ add_compile_definitions(WAIRTOHUB)
 add_executable(jacktrip ${qjacktrip_SRC})
 
 # Set our libraries for our linker
-set (qjacktrip_LIBS Qt5::Widgets)
+set (qjacktrip_LIBS Qt5::Network)
 if (NOT nogui)
-  set (qjacktrip_LIBS ${qjacktrip_LIBS} Qt5::Network)
+  set (qjacktrip_LIBS ${qjacktrip_LIBS} Qt5::Widgets)
+  if (NOT novs)
+    set (qjacktrip_LIBS ${qjacktrip_LIBS} Qt5::Quick Qt5::NetworkAuth)
+  endif ()
 endif ()
 
 if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
diff --git a/build b/build
index 1f8d5ca6f19b7e3f1231833abd9a243a979678f2..b4141be34a81c7941c97980326b07f1340b010b2 100755 (executable)
--- a/build
+++ b/build
@@ -11,13 +11,15 @@ RTAUDIO=0
 NO_SYSTEM_RTAUDIO=0
 PRO_FILE="../jacktrip.pro"
 HELP_STR="usage:\n
-./build [noclean nojack rtaudio nogui static install [-config static]]\n\n
+./build [noclean nojack rtaudio nogui novs static install [-config static]]\n\n
 options:\n
 noclean - do not run \"make clean\" first\n
 nojack - build without jack\n
 rtaudio - build with RtAudio\n
 no-system-rtaudio - use bundled RtAudio library even if it's available in the system\n
 nogui - build without the gui\n
+novs - build without Virtual Studio support\n
+noupdater - build without auto-update support\n
 static - build with static libraries\n
 weakjack - build with weak linking of jack libraries\n
 install - install jacktrip in system location (uses sudo)\n
@@ -40,6 +42,14 @@ while [[ "$#" -gt 0 ]]; do
       echo "Building without the gui"
       CONFIG="-config nogui $CONFIG" 
       ;;
+      novs)
+      echo "Building without Virtual Studio support"
+      CONFIG="-config novs $CONFIG" 
+      ;;
+      noupdater)
+      echo "Building without auto-update support"
+      CONFIG="-config noupdater $CONFIG"
+      ;;
       static)
       echo "Building with static libraries"
       CONFIG="-config static $CONFIG" 
@@ -66,6 +76,9 @@ while [[ "$#" -gt 0 ]]; do
   fi
 done
 
+echo "All build options:"
+echo "$CONFIG"
+
 # Check for Platform
 platform='unknown'
 unamestr=`uname`
@@ -99,6 +112,8 @@ elif [[ $platform == 'macosx' ]]; then
     QSPEC=macx-clang-arm64
   fi  
   # if qmake is not in path, try homebrew qt5
+  echo "path to qmake"
+  echo "$(which qmake)"
   if ! command -v $QCMD &> /dev/null; then
     # echo "qmake not found in \$PATH, searching for homebrew qt5"
     QT_PREFIX=`brew --prefix qt5`
index 698c760ff714f960e8770eff13bf81af06e5d046..b722e21fa7281de8bf6ab00ef4c57609ffcda28e 100644 (file)
@@ -16,7 +16,7 @@ Optional:
 
 ### Fedora
 ```sh
-dnf install qt5-devel
+dnf install qt5-qtbase-devel qt5-qtnetworkauth-devel qt5-qtquickcontrols2-devel qt5-qtsvg-devel
 dnf groupinstall "C Development Tools and Libraries"
 dnf groupinstall "Development Tools"
 dnf install "pkgconfig(jack)" alsa-lib-devel git help2man
@@ -29,7 +29,7 @@ directory or use QtCreator to compile.
 ### Ubuntu and Debian/Raspbian
 ```sh
 apt install --no-install-recommends build-essential qt5-default autoconf automake libtool make libjack-jackd2-dev git help2man
-apt install qjackctl
+apt install qjackctl qt5-qmake qttools5-dev libqt5svg5-dev libqt5networkauth5-dev qtdeclarative5-dev qml-module-qtquick-controls
 apt install librtaudio-dev # if building with RtAudio
 ```
 
index 69c0428bee34c503b3aa2fe58314a0700ac57cac..898361f2e1373d6e8185ed1f1bb273aa53ab708e 100644 (file)
@@ -72,7 +72,7 @@ To build using QtCreator:
   * Open jacktrip.pro using QtCreator
   * Choose a correctly configured Kit
 
-QtCreator places the `jacktrip` executabe by default in a folder
+QtCreator places the `jacktrip` executable by default in a folder
 with a name like `build-jacktrip-Desktop_x86_darwin_generic_mach_o_64bit-Release/`.
 
 ## Installation
index 3ece86199572fa876d4e985d24040dd2e3b7d175..5ea4037c12b35fe759b45e82e43239290ee9ab24 100644 (file)
@@ -20,7 +20,7 @@ find its documentation at [mesonbuild.com](https://mesonbuild.com/).
 === "MacOS"
 
     ```bash
-    brew install meson qt5 rt-audio help2man
+    brew install meson qt5 rtaudio help2man
     ```
     
     You also need to install Jack, unless you want to disable jack support
@@ -47,7 +47,7 @@ Meson shows you also the options of subprojects like RtAudio.
 ## Build
 
 Meson builds in a separate directory. It doesn't touch anything of your project.
-This way you can have seperate debug and release build directories for example. 
+This way you can have separate debug and release build directories for example. 
 
 Prepare your build directory:
 ```bash
index d0d602319d5bfc5d3bf4673f537aa6e4cae81707..be39c91a3f2e5b0743a9de341433492c572579db 100644 (file)
@@ -1,3 +1,12 @@
+- Version: "1.6.0"
+  Date: 2022-05-30
+  Description:
+  - (added) Virtual Studio integration; previous GUI is now called "Classic Mode"
+  - (added) dblsqd for auto-updates
+  - (updated) buffer strategy 3 - multiple updates and fixes, still experimental
+  - (added) JackTrip Labs signing scripts
+  - (fixed) OpenSSL in the build script
+  - (updated) code cleanup and maintenance
 - Version: "1.5.3"
   Date: 2022-03-28
   Description:
index 8c7259fa5a357840ce85578bfe08aea31f660215..6b3a6470d7a291427b5c9cccbecd42908d1b59d5 100644 (file)
@@ -14,9 +14,8 @@
 //----------------------------------------------------------------------------
 //  FAUST Generated Code
 //----------------------------------------------------------------------------
-// clang-format off
+
 <<includeIntrinsic>>
 
-    <<includeclass>>
+<<includeclass>>
 
-    // clang-format on
index b98a83c38fc42011d0db99be4c66a367dce52587..d709c6dc871c69a718188b4883a6318fa7d51f7c 100644 (file)
@@ -4,6 +4,9 @@ import("stdfaust.lib");
 
 process = zita_rev1; // same as dm.zita_rev1 but for wetness control and some defaults
 
+//process = zita_rev1 : _,attach(cout); // Not using this solution yet, but it works
+//cout = ffunction (int cout(), <iostream>, ""); // dummy function to force #include <iostream> in output
+
 //----------------------------------`(dm.)zita_rev1`------------------------------
 // Example GUI for `zita_rev1_stereo` (mostly following the Linux `zita-rev1` GUI).
 //
index 0b2a5ee3b07b1433e9b179e2b50f56d770b53120..106e37d4e8836091700a0014ab2ff7b1f2c74d14 100644 (file)
@@ -17,7 +17,7 @@ CONFIG(debug, debug|release) {
 }
 
 equals(QT_EDITION, "OpenSource") {
-    DEFINES += QT_OPENSOURCE
+  DEFINES += QT_OPENSOURCE
 }
 
 nogui {
@@ -26,6 +26,17 @@ nogui {
 } else {
   QT += gui
   QT += widgets
+  novs {
+    DEFINES += NO_VS
+  } else {
+    QT += networkauth
+    QT += qml
+    QT += quick
+    QT += svg
+  }
+  noupdater {
+    DEFINES += NO_UPDATER
+  }
 }
 
 QT += network
@@ -44,9 +55,6 @@ nojack {
   DEFINES += NO_JACK
 }
 
-# for plugins
-INCLUDEPATH += faust-src-lair/stk
-
 !win32 {
   INCLUDEPATH+=/usr/local/include
 # wair needs stk, can be had from linux this way
@@ -140,7 +148,6 @@ linux-g++-64 {
   message(Linux 64bit)
 }
 
-
 win32 {
   message(Building on win32)
 #cc  CONFIG += x86 console
@@ -184,14 +191,11 @@ QMAKE_CLEAN += -r ./jacktrip ./jacktrip_debug ./release/* ./debug/* ./$${applica
 # isEmpty(PREFIX) will allow path to be changed during the command line
 # call to qmake, e.g. qmake PREFIX=/usr
 isEmpty(PREFIX) {
- PREFIX = /usr/local
 PREFIX = /usr/local
 }
 target.path = $$PREFIX/bin/
 INSTALLS += target
 
-# for plugins
-INCLUDEPATH += faust-src-lair
-
 # Input
 HEADERS += src/DataProtocol.h \
            src/JackTrip.h \
@@ -223,20 +227,31 @@ HEADERS += src/DataProtocol.h \
 #(Removed JackTripThread.h JackTripWorkerMessages.h NetKS.h TestRingBuffer.h ThreadPoolTest.h)
 
 !nojack {
-HEADERS += src/JackAudioInterface.h \
-           src/JMess.h \
-           src/Patcher.h
+  HEADERS += src/JackAudioInterface.h \
+             src/JMess.h \
+             src/Patcher.h
 }
 
 !nogui {
-HEADERS += src/gui/about.h \
-           src/gui/messageDialog.h \
-           src/gui/qjacktrip.h \
-           src/gui/textbuf.h
+  HEADERS += src/gui/about.h \
+             src/gui/messageDialog.h \
+             src/gui/qjacktrip.h \
+             src/gui/textbuf.h
+  !novs {
+    HEADERS += src/gui/virtualstudio.h \
+               src/gui/vsServerInfo.h \
+               src/gui/vsQuickView.h
+  }
+  !noupdater {
+    HEADERS += src/dblsqd/feed.h \
+               src/dblsqd/release.h \
+               src/dblsqd/semver.h \
+               src/dblsqd/update_dialog.h
+  }
 }
 
 rtaudio|bundled_rtaudio {
-    HEADERS += src/RtAudioInterface.h
+  HEADERS += src/RtAudioInterface.h
 }
 
 SOURCES += src/DataProtocol.cpp \
@@ -262,16 +277,27 @@ SOURCES += src/DataProtocol.cpp \
 #(Removed jacktrip_main.cpp jacktrip_tests.cpp JackTripThread.cpp ProcessPlugin.cpp)
 
 !nojack {
-SOURCES += src/JackAudioInterface.cpp \
-           src/JMess.cpp \
-           src/Patcher.cpp
+  SOURCES += src/JackAudioInterface.cpp \
+             src/JMess.cpp \
+             src/Patcher.cpp
 }
 
 !nogui {
-SOURCES += src/gui/messageDialog.cpp \
-           src/gui/qjacktrip.cpp \
-           src/gui/about.cpp \
-           src/gui/textbuf.cpp
+  SOURCES += src/gui/messageDialog.cpp \
+             src/gui/qjacktrip.cpp \
+             src/gui/about.cpp \
+             src/gui/textbuf.cpp
+  !novs {
+    SOURCES += src/gui/virtualstudio.cpp \
+               src/gui/vsServerInfo.cpp \
+               src/gui/vsQuickView.cpp
+  }
+  !noupdater {
+    SOURCES += src/dblsqd/feed.cpp \
+               src/dblsqd/release.cpp \
+               src/dblsqd/semver.cpp \
+               src/dblsqd/update_dialog.cpp
+  }
 }
 
 !nogui {
@@ -279,13 +305,19 @@ SOURCES += src/gui/messageDialog.cpp \
     HEADERS += src/gui/NoNap.h
     OBJECTIVE_SOURCES += src/gui/NoNap.mm
   }
-
   FORMS += src/gui/qjacktrip.ui src/gui/about.ui src/gui/messageDialog.ui
-  RESOURCES += src/gui/qjacktrip.qrc
+  novs {
+    RESOURCES += src/gui/qjacktrip_novs.qrc
+  } else {
+    RESOURCES += src/gui/qjacktrip.qrc
+  }
+  !noupdater {
+    FORMS += src/dblsqd/update_dialog.ui
+  }
 }
 
 rtaudio|bundled_rtaudio {
-    SOURCES += src/RtAudioInterface.cpp
+  SOURCES += src/RtAudioInterface.cpp
 }
 
 weakjack {
index 8fe947ca2f38af41ca252e209dc4a7f6b35d93f4..ef0cb43f4f475cd630a788f1c7be2f7923591ffa 100644 (file)
@@ -1,6 +1,6 @@
 app-id: org.jacktrip.JackTrip.Devel
 runtime: org.kde.Platform
-runtime-version: '5.15'
+runtime-version: '5.15-21.08'
 sdk: org.kde.Sdk
 command: jacktrip
 finish-args:
@@ -9,12 +9,12 @@ finish-args:
   - --socket=x11
   # Wayland access
   # - --socket=wayland
+  # OpenGL
+  - --device=dri
   # Needs network access
   - --share=network
   # Pipewire/Jack
   - --filesystem=xdg-run/pipewire-0
-  # For setting realtime priority for network thread?
-  - --socket=system-bus
 cleanup:
   - /lib/python3.8
   - /share/man
@@ -27,20 +27,20 @@ modules:
     - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "pyyaml" --no-build-isolation
     sources:
     - type: file
-      sha256: 68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2
       url: https://files.pythonhosted.org/packages/36/2b/61d51a2c4f25ef062ae3f74576b01638bebad5e045f747ff12643df63844/PyYAML-6.0.tar.gz
+      sha256: 68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2
   - name: python3-jinja2
     buildsystem: simple
     cleanup: [ "*" ]
     build-commands:
     - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "jinja2" --no-build-isolation
     sources:
-    - sha256: 594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a
-      type: file
-      url: https://files.pythonhosted.org/packages/bf/10/ff66fea6d1788c458663a84d88787bae15d45daa16f6b3ef33322a51fc7e/MarkupSafe-2.0.1.tar.gz
-    - sha256: 077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8
-      type: file
-      url: https://files.pythonhosted.org/packages/20/9a/e5d9ec41927401e41aea8af6d16e78b5e612bca4699d417f646a9610a076/Jinja2-3.0.3-py3-none-any.whl
+    - type: file
+      url: https://files.pythonhosted.org/packages/1d/97/2288fe498044284f39ab8950703e88abbac2abbdf65524d576157af70556/MarkupSafe-2.1.1.tar.gz
+      sha256: 7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b
+    - type: file
+      url: https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl
+      sha256: 6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
   - name: jacktrip
     buildsystem: meson
     config-opts:
index 93c7f2815ff97c14c50a5feaeb56e47a151d1268..b2d211c7eb32085e29ee3fd5a5b513ec9e93f15a 100644 (file)
@@ -1,20 +1,20 @@
 app-id: org.jacktrip.JackTrip.Devel
 runtime: org.kde.Platform
-runtime-version: '5.15'
+runtime-version: '5.15-21.08'
 sdk: org.kde.Sdk
 command: jacktrip
 finish-args:
   # X11 + XShm access
   - --share=ipc
   - --socket=x11
-  # Wayland access (disabled because of missing window shadows)
+  # Wayland access
   # - --socket=wayland
+  # OpenGL
+  - --device=dri
   # Needs network access
   - --share=network
   # Pipewire/Jack
   - --filesystem=xdg-run/pipewire-0
-  # For setting realtime priority for network thread?
-  - --socket=system-bus
 cleanup:
   - /lib/python3.8
   - /share/man
@@ -27,20 +27,20 @@ modules:
     - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "pyyaml" --no-build-isolation
     sources:
     - type: file
-      sha256: 68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2
       url: https://files.pythonhosted.org/packages/36/2b/61d51a2c4f25ef062ae3f74576b01638bebad5e045f747ff12643df63844/PyYAML-6.0.tar.gz
+      sha256: 68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2
   - name: python3-jinja2
     buildsystem: simple
     cleanup: [ "*" ]
     build-commands:
     - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "jinja2" --no-build-isolation
     sources:
-    - sha256: 594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a
-      type: file
-      url: https://files.pythonhosted.org/packages/bf/10/ff66fea6d1788c458663a84d88787bae15d45daa16f6b3ef33322a51fc7e/MarkupSafe-2.0.1.tar.gz
-    - sha256: 077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8
-      type: file
-      url: https://files.pythonhosted.org/packages/20/9a/e5d9ec41927401e41aea8af6d16e78b5e612bca4699d417f646a9610a076/Jinja2-3.0.3-py3-none-any.whl
+    - type: file
+      url: https://files.pythonhosted.org/packages/1d/97/2288fe498044284f39ab8950703e88abbac2abbdf65524d576157af70556/MarkupSafe-2.1.1.tar.gz
+      sha256: 7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b
+    - type: file
+      url: https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl
+      sha256: 6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
   - name: jacktrip
     buildsystem: meson
     config-opts:
index eda815278ca5f659458448911b4afe0e0bd22dc8..9bdfcc527b859fe6a186496a3697b9e0c54cf11c 100644 (file)
@@ -1,6 +1,6 @@
 app-id: org.jacktrip.JackTrip
 runtime: org.kde.Platform
-runtime-version: '5.15'
+runtime-version: '5.15-21.08'
 sdk: org.kde.Sdk
 command: jacktrip
 finish-args:
@@ -9,12 +9,12 @@ finish-args:
   - --socket=x11
   # Wayland access
   # - --socket=wayland
+  # OpenGL
+  - --device=dri
   # Needs network access
   - --share=network
   # Pipewire/Jack
   - --filesystem=xdg-run/pipewire-0
-  # For setting realtime priority for network thread?
-  - --socket=system-bus
 cleanup:
   - /lib/python3.8
   - /share/man
@@ -27,20 +27,20 @@ modules:
     - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "pyyaml" --no-build-isolation
     sources:
     - type: file
-      sha256: 68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2
       url: https://files.pythonhosted.org/packages/36/2b/61d51a2c4f25ef062ae3f74576b01638bebad5e045f747ff12643df63844/PyYAML-6.0.tar.gz
+      sha256: 68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2
   - name: python3-jinja2
     buildsystem: simple
     cleanup: [ "*" ]
     build-commands:
     - pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}" --prefix=${FLATPAK_DEST} "jinja2" --no-build-isolation
     sources:
-    - sha256: 594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a
-      type: file
-      url: https://files.pythonhosted.org/packages/bf/10/ff66fea6d1788c458663a84d88787bae15d45daa16f6b3ef33322a51fc7e/MarkupSafe-2.0.1.tar.gz
-    - sha256: 077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8
-      type: file
-      url: https://files.pythonhosted.org/packages/20/9a/e5d9ec41927401e41aea8af6d16e78b5e612bca4699d417f646a9610a076/Jinja2-3.0.3-py3-none-any.whl
+    - type: file
+      url: https://files.pythonhosted.org/packages/1d/97/2288fe498044284f39ab8950703e88abbac2abbdf65524d576157af70556/MarkupSafe-2.1.1.tar.gz
+      sha256: 7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b
+    - type: file
+      url: https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl
+      sha256: 6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
   - name: jacktrip
     buildsystem: meson
     sources:
index 58cd87ede7d2ea7387b20c33713b4db7f4b10df6..0ca37fd1b90682f086d02836e29389ac1237f821 100644 (file)
@@ -27,7 +27,7 @@
                <string>MacOSX</string>
        </array>
        <key>CFBundleVersion</key>
-       <string>16</string>
+       <string>%VERSION%</string>
        <key>DTCompiler</key>
        <string>com.apple.compilers.llvm.clang.1_0</string>
        <key>DTPlatformBuild</key>
index aa7c4b47f3eedf1079819dd6d1723cbc618750ee..6318bb7a04e8b902e84db40116bb570ce95563e9 100755 (executable)
@@ -47,7 +47,7 @@ while getopts ":inhc:d:u:p:a:b:" opt; do
       h)
         echo "JackTrip App Bundle assembly script."
         echo "Copyright (C) 2020-2021 Aaron Wyatt et al."
-        echo "Relased under the GNU GPLv3 License."
+        echo "Released under the GNU GPLv3 License."
         echo
         echo "Usage: ./assemble-app.sh [options] [appname] [bundlename]"
         echo
@@ -114,10 +114,15 @@ if [ ! -z "$DYNAMIC_QT" ]; then
             exit 1
         fi
     fi
+    VS=$(otool -L ../builddir/jacktrip | grep QtQml)
+    QMLDIR=""
+    if [ ! -z "VS" ]; then
+        QMLDIR=" -qmldir=../src/gui"
+    fi
     if [ ! -z "$CERTIFICATE" ]; then
-        $DEPLOY_CMD "$APPNAME.app" -codesign="$CERTIFICATE"
+        $DEPLOY_CMD "$APPNAME.app"$QMLDIR -codesign="$CERTIFICATE"
     else
-        $DEPLOY_CMD "$APPNAME.app"
+        $DEPLOY_CMD "$APPNAME.app"$QMLDIR
     fi
 fi
 
index f5f2af4b790d98115c90659856e556539caee9dc..18a6e56c76d4952a5766723aa363b6c1e70654d3 100644 (file)
@@ -42,7 +42,7 @@
                                                                        <key>BUNDLE_POSTINSTALL_PATH</key>
                                                                        <dict>
                                                                                <key>PATH</key>
-                                                                               <string>link.sh</string>
+                                                                               <string>postinstall.sh</string>
                                                                                <key>PATH_TYPE</key>
                                                                                <integer>1</integer>
                                                                        </dict>
diff --git a/macos/package/link.sh b/macos/package/link.sh
deleted file mode 100644 (file)
index 350da28..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/sh
-
-mkdir -p /usr/local/bin
-rm -f /usr/local/bin/jacktrip
-ln -s "$2"/Contents/MacOS/jacktrip /usr/local/bin/jacktrip
diff --git a/macos/package/postinstall.sh b/macos/package/postinstall.sh
new file mode 100755 (executable)
index 0000000..a668d3c
--- /dev/null
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+# Link jacktrip to app binary
+mkdir -p /usr/local/bin
+rm -f /usr/local/bin/jacktrip
+ln -s "$2"/Contents/MacOS/jacktrip /usr/local/bin/jacktrip
+
+# Open JackTrip on intaller finish
+open -a /Applications/JackTrip.app
+exit 0
index 4293f568755d6eafd55be54fa8318822e71ea6c4..4c36da9ec333c1caa9a3f0711acdd4064c842dd1 100644 (file)
@@ -1,5 +1,5 @@
 project('jacktrip', ['cpp','c'],
-               default_options: ['cpp_std=c++17','warning_level=2'])
+               default_options: ['cpp_std=c++17','warning_level=2','optimization=2'])
 
 if get_option('profile') == 'development'
   application_id = 'org.jacktrip.JackTrip.Devel'
@@ -80,19 +80,60 @@ if get_option('nogui') == true
        defines += '-DNO_GUI'
        qt5_dep = dependency('qt5', modules: ['Core', 'Network'], include_type: 'system')
 else
-       qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets'], include_type: 'system')
-       src += ['src/gui/qjacktrip.cpp',
+       src += [
+               'src/gui/qjacktrip.cpp',
                'src/gui/about.cpp',
                'src/gui/messageDialog.cpp',
-               'src/gui/textbuf.cpp']
-       ui_h += ['src/gui/qjacktrip.ui',
-               'src/gui/messageDialog.ui',
-               'src/gui/about.ui']
-       moc_h += ['src/gui/about.h',
+               'src/gui/textbuf.cpp'
+       ]
+       moc_h += [
+               'src/gui/about.h',
                'src/gui/qjacktrip.h',
                'src/gui/messageDialog.h',
-               'src/gui/textbuf.h']
-       qres = ['src/gui/qjacktrip.qrc']
+               'src/gui/textbuf.h'
+       ]
+       ui_h += [
+               'src/gui/qjacktrip.ui',
+               'src/gui/messageDialog.ui',
+               'src/gui/about.ui'
+       ]
+
+       if get_option('novs') == true
+               defines += '-DNO_VS'
+               qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets'], include_type: 'system')
+               qres = ['src/gui/qjacktrip_novs.qrc']
+       else
+               src += [
+                       'src/gui/virtualstudio.cpp',
+                       'src/gui/vsServerInfo.cpp',
+                       'src/gui/vsQuickView.cpp'
+               ]
+               moc_h += [
+                       'src/gui/virtualstudio.h',
+                       'src/gui/vsServerInfo.h',
+                       'src/gui/vsQuickView.h'
+               ]
+               qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Quick', 'Qml', 'Svg', 'NetworkAuth'], include_type: 'system')
+               qres = ['src/gui/qjacktrip.qrc']
+       endif
+
+       if get_option('noupdater') == true
+               defines += '-DNO_UPDATER'
+       else
+               src += [
+                       'src/dblsqd/feed.cpp',
+                       'src/dblsqd/release.cpp',
+                       'src/dblsqd/semver.cpp',
+                       'src/dblsqd/update_dialog.cpp'
+               ]
+               moc_h += [
+                       'src/dblsqd/feed.h',
+                       'src/dblsqd/release.h',
+                       'src/dblsqd/semver.h',
+                       'src/dblsqd/update_dialog.h'
+               ]
+               ui_h += ['src/dblsqd/update_dialog.ui']
+       endif
 endif
 deps += qt5_dep
 
index 6d93cd730cdd16e40bc3a4f6fe9dc3a2d4af2fe7..242ea80f5eddd1daae719042f410d1f531d1c58b 100644 (file)
@@ -3,4 +3,6 @@ option('rtaudio', type : 'feature', value : 'auto', description: 'Build with RtA
 option('jack', type : 'feature', value : 'auto', description: 'Build with JACK Backend')
 option('weakjack', type : 'boolean', value : 'false', description: 'Weak link JACK library')
 option('nogui', type : 'boolean', value : 'false', description: 'Build without graphical user interface')
+option('novs', type : 'boolean', value : 'false', description: 'Build without Virtual Studio support')
+option('noupdater', type : 'boolean', value : 'false', description: 'Build without auto-update support')
 option('profile', type: 'combo', choices: ['default', 'development'], value: 'default', description: 'Choose build profile / Sets desktop id accordingly')
diff --git a/releases/edge/mac-manifests.json b/releases/edge/mac-manifests.json
new file mode 100644 (file)
index 0000000..97de59d
--- /dev/null
@@ -0,0 +1,65 @@
+{
+    "app_name": "JackTrip",
+    "releases": [
+        {
+            "version": "1.6.0",
+            "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0",
+            "download": {
+                "date": "2022-06-01T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0/JackTrip-v1.6.0-macOS-x64-installer.pkg",
+                "downloadSize": 11474299,
+                "sha256": "27259600ecd879106ebbf97754d72d6236075a049eafa0de6271d33f753f13e4"
+            }
+        },
+        {
+            "version": "1.6.0-rc.5",
+            "changelog": "Release candidate 5 for 1.6.0",
+            "download": {
+                "date": "2022-05-30T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.5/JackTrip-v1.6.0-rc.5-macOS-x64-installer.pkg",
+                "downloadSize": 11474262,
+                "sha256": "8289530a8e6ef1f772776c7078679e2dac146f366cfc4e8c09e0ad16865fe274"
+            }
+        },
+        {
+            "version": "1.6.0-rc.4",
+            "changelog": "Release candidate 4 for 1.6.0",
+            "download": {
+                "date": "2022-05-29T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.4/JackTrip-v1.6.0-rc.4-macOS-x64-installer.pkg",
+                "downloadSize": 11460550,
+                "sha256": "38d817f3e8cc61b707392ce74cee8ab46da9c8eb2086ea2b3f0c79496caf70a8"
+            }
+        },
+        {
+            "version": "1.6.0-rc.3",
+            "changelog": "Release candidate 3 for 1.6.0",
+            "download": {
+                "date": "2022-05-27T00:00:00Z",
+                "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-rc.3-macOS-x64-installer.pkg",
+                "downloadSize": 11460230,
+                "sha256": "c9614964974d61c062d905f01c7d30ab04a697562ecfba6264392aebe7161051"
+            }
+        },
+        {
+            "version": "1.6.0-rc.2",
+            "changelog": "Release candidate 2 for 1.6.0",
+            "download": {
+                "date": "2022-05-26T00:00:00Z",
+                "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-rc.2-macOS-x64-signed-installer.pkg",
+                "downloadSize": 11460155,
+                "sha256": "ad508680115f73036da3a5328ddf0841b86620406406e0ffaa4b982e24a27771"
+            }
+        },
+        {
+            "version": "1.6.0-rc.1",
+            "changelog": "Release candidate 1 for 1.6.0",
+            "download": {
+                "date": "2022-05-23T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.1/JackTrip-v1.6.0-rc.1-macOS-x64-installer.pkg",
+                "downloadSize": 11076221,
+                "sha256": "071cda0ce59361e474a04db00beec41e92d2d823dab71e3fab179faf89f6fd7e"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/releases/edge/win-manifests.json b/releases/edge/win-manifests.json
new file mode 100644 (file)
index 0000000..59b8186
--- /dev/null
@@ -0,0 +1,65 @@
+{
+    "app_name": "JackTrip",
+    "releases": [
+        {
+            "version": "1.6.0",
+            "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0",
+            "download": {
+                "date": "2022-06-01T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0/JackTrip-v1.6.0-Windows-x64-installer.msi",
+                "downloadSize": 43364352,
+                "sha256": "9562ab654202bfc432e05caa3bd2bf1d0b52c50581b0a567f0546983fe46c078"
+            }
+        },
+        {
+            "version": "1.6.0-rc.5",
+            "changelog": "Release candidate 5 for 1.6.0",
+            "download": {
+                "date": "2022-05-30T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.5/JackTrip-v1.6.0-rc.5-Windows-x64-installer.msi",
+                "downloadSize": 43364352,
+                "sha256": "d84e6e5d21cf31f5dd48e9dcc0c1a44fe7a37d977f94b6ff63d5e381745e5a44"
+            }
+        },
+        {
+            "version": "1.6.0-rc.4",
+            "changelog": "Release candidate 4 for 1.6.0",
+            "download": {
+                "date": "2022-05-29T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.4/JackTrip-v1.6.0-rc.4-Windows-x64-installer.msi",
+                "downloadSize": 43126784,
+                "sha256": "cdb0ef906cf0d6047289838bf013b31a626cdd74dd4f75d6c1c4c3adbc9cd41d"
+            }
+        },
+        {
+            "version": "1.6.0-rc.3",
+            "changelog": "Release candidate 3 for 1.6.0",
+            "download": {
+                "date": "2022-05-27T00:00:00Z",
+                "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-rc.3-Windows-x64-installer.msi",
+                "downloadSize": 43118592,
+                "sha256": "dceaf670a67cf1541007db82c5ce937b25370a7140e48192b94470f575fc4988"
+            }
+        },
+        {
+            "version": "1.6.0-rc.2",
+            "changelog": "Release candidate 2 for 1.6.0",
+            "download": {
+                "date": "2022-05-26T00:00:00Z",
+                "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-rc.2-Windows-x64-signed-installer.msi",
+                "downloadSize": 43114496,
+                "sha256": "b1a7adc8dc0fb47f59515790e8531dd10838d799bacb4b5653192ed621bca208"
+            }
+        },
+        {
+            "version": "1.6.0-rc.1",
+            "changelog": "Release candidate 1 for 1.6.0",
+            "download": {
+                "date": "2022-05-23T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.1/JackTrip-v1.6.0-rc.1-Windows-x64-installer.msi",
+                "downloadSize": 43081728,
+                "sha256": "240f8b495ec5057228be922da80829a3718b474bafc2ba2d77750643abd1005c"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/releases/stable/mac-manifests.json b/releases/stable/mac-manifests.json
new file mode 100644 (file)
index 0000000..0faec7c
--- /dev/null
@@ -0,0 +1,15 @@
+{
+    "app_name": "JackTrip",
+    "releases": [
+        {
+            "version": "1.6.0",
+            "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0",
+            "download": {
+                "date": "2022-06-01T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0/JackTrip-v1.6.0-macOS-x64-installer.pkg",
+                "downloadSize": 11474299,
+                "sha256": "27259600ecd879106ebbf97754d72d6236075a049eafa0de6271d33f753f13e4"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/releases/stable/win-manifests.json b/releases/stable/win-manifests.json
new file mode 100644 (file)
index 0000000..cb255c0
--- /dev/null
@@ -0,0 +1,15 @@
+{
+    "app_name": "JackTrip",
+    "releases": [
+        {
+            "version": "1.6.0",
+            "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0",
+            "download": {
+                "date": "2022-06-01T00:00:00Z",
+                "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0/JackTrip-v1.6.0-Windows-x64-installer.msi",
+                "downloadSize": 43364352,
+                "sha256": "9562ab654202bfc432e05caa3bd2bf1d0b52c50581b0a567f0546983fe46c078"
+            }
+        }
+    ]
+}
\ No newline at end of file
index ff499802c429d84f0c0021664073a37495d80d17..3c3fc1d9b681327befc30d1d21b4e4013f260045 100755 (executable)
@@ -48,7 +48,7 @@ if [ $JACKD != 0 ]
 # killall jackd
     if [ "$(ps -aux | grep -c jackd)" != 1 ]; then killall jackd; fi;
 # if jackd is or has been running with another driver
-# much experimenation shows it literally takes this long
+# much experimentation shows it literally takes this long
     sleep 17
 # to flush old connections before starting the dummy driver
 
index ba6547785ce6f59179425d8ff3860b32a891fa13..a6dc55be7c558791381208a1c4333b047e206b0f 100644 (file)
@@ -160,7 +160,7 @@ void AudioInterface::setup()
     mAudioInputPacket  = new int8_t[size_audio_input];
     mAudioOutputPacket = new int8_t[size_audio_output];
 
-    // Initialize and asign memory for ProcessPlugins Buffers
+    // Initialize and assign memory for ProcessPlugins Buffers
 #ifdef WAIR  // WAIR
     if (mNumNetRevChans) {
         mInProcessBuffer.resize(mNumNetRevChans);
index db28f42ba82abc5e8e1b743af477729e5f27a042..293e84a2689b07dde822bd6bad72bca0edeb9d10 100644 (file)
@@ -109,9 +109,9 @@ class AudioInterface
     /** \brief Process callback. Subclass should call this callback after obtaining the
     in_buffer and out_buffer pointers.
     * \param in_buffer Array of input audio samplers for each channel. The user
-    * is reponsible to check that each channel has n_frames samplers
+    * is responsible to check that each channel has n_frames samplers
     * \param in_buffer Array of output audio samplers for each channel. The user
-    * is reponsible to check that each channel has n_frames samplers
+    * is responsible to check that each channel has n_frames samplers
     */
     virtual void broadcastCallback(QVarLengthArray<sample_t*>& mon_buffer,
                                    unsigned int n_frames);
index 3cbd35197374ca448fcac9a408a9588d94162573..40eaeef00fc6859cea4cd6217eb29d7b43eeb8c2 100644 (file)
@@ -112,7 +112,7 @@ void Auth::loadAuthFile(const QString& filename)
                 continue;
             }
 
-            // Check that our password hash is useable.
+            // Check that our password hash is usable.
             bool invalid = false;
             if (lineParts.at(1).startsWith(QLatin1String("$6$"))) {
                 QStringList hashParts = lineParts.at(1).split(QStringLiteral("$"));
@@ -185,7 +185,7 @@ bool Auth::checkTime(const QString& username)
 
 char Auth::char64(int value)
 {
-    // Returns a base 64 enconding using the following characters:
+    // Returns a base 64 encoding using the following characters:
     // ./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
     if (value < 0 || value >= 64) {
         return 0;
index 954a749e6bfb18f62d1488b2105aba84a5a1db1b..e0cb3245c3a8c08ff358e8eae7e7a7936f730284 100644 (file)
@@ -37,8 +37,6 @@
 
 #include "Compressor.h"
 
-#include <iostream>
-
 //*******************************************************************************
 void Compressor::compute(int nframes, float** inputs, float** outputs)
 {
index 120877921d638efe8879618bb41cba68b056077d..84fe58e89091b7440552eba43d50528f72c964d1 100644 (file)
@@ -41,6 +41,7 @@
 #ifndef __COMPRESSOR_H__
 #define __COMPRESSOR_H__
 
+#include <iostream>
 #include <vector>
 
 #include "CompressorPresets.h"
index b01643fef60d64668a7b0840d930c10950e25a72..38755b691e9c14fcb1f0922d0c1ff9a18fc96629 100644 (file)
@@ -153,7 +153,7 @@ class DataProtocol : public QThread
      */
     virtual void setPeerAddress(const char* peerHostOrIP) = 0;
 
-    /** \brief Set the peer incomming (receiving) port number
+    /** \brief Set the peer incoming (receiving) port number
      * \param port Port number
      * \todo implement here instead of in the subclass UDP
      */
index 85c6029c6df232361c10a26a871be1e7d54db0bf..8517334fae03a09159cd858bfffa766c13567448 100644 (file)
@@ -150,7 +150,7 @@ void JMess::connectTUB(int /*nChans*/)
             }
 
             // SC to jacktrip
-            tmp += 4;  // increase tmp for port offest
+            tmp += 4;  // increase tmp for port offset
             qDebug() << "connect " << serverAudio << HARDWIRED_AUDIO_PROCESS_ON_SERVER_OUT
                      << tmp << "with " << client << ":send_" << l;
 
index afb78c30901eba0115ccfa1ae980a21cc3433ad0..08b602b68bf2ff2424fb01433f12b2449d780a74 100644 (file)
@@ -50,7 +50,7 @@ const int Indent = 2;
 /*! \brief Class to save and load all jack client connections.
  *
  * Saves an XML file with all the current jack connections. This same file can
- * be loaded to connect evrything again. The XML file can also be edited.
+ * be loaded to connect everything again. The XML file can also be edited.
  *
  * Has also an option to disconnect all the clients.
  */
index 44884f1a50c529781a96f82f0272847d0c786f42..a3f663319a1eb80129eeb0306645a2a83a101579 100644 (file)
@@ -165,8 +165,8 @@ class JackAudioInterface : public AudioInterface
      *
      * <tt>jack_set_process_callback</tt> needs a static member function pointer. A normal
      * member function won't work because a <b><i>this</i></b> pointer is passed under the
-     * scenes. That's why we need to cast the member funcion processCallback to the static
-     * function wrapperProcessCallback. The callback is then set as:\n
+     * scenes. That's why we need to cast the member function processCallback to the
+     * static function wrapperProcessCallback. The callback is then set as:\n
      * <tt>jack_set_process_callback(mClient, JackAudioInterface::wrapperProcessCallback,
      *                              this)</tt>
      */
index e841731719e202f1592561bd6eb46c8eab3edd3e..a5475bb8f84f7eb91a9dd040173f08d5e59847a3 100644 (file)
@@ -168,8 +168,8 @@ void JackTrip::setupAudio(
 {
     // Check if mAudioInterface has already been created or not
     if (mAudioInterface
-        != NULL) {  // if it has been created, disconnet it from JACK and delete it
-        cout << "WARINING: JackAudio interface was setup already:" << endl;
+        != NULL) {  // if it has been created, disconnect it from JACK and delete it
+        cout << "WARNING: JackAudio interface was setup already:" << endl;
         cout << "It will be erased and setup again." << endl;
         cout << gPrintSeparator << endl;
         closeAudio();
@@ -377,15 +377,20 @@ void JackTrip::setupRingBuffers()
                 new RingBuffer(audio_output_slot_size, mBufferQueueLength);
             mPacketHeader->setBufferRequiresSameSettings(true);
         } else if (mBufferStrategy == 3) {
-            qDebug() << "experimental buffer strategy 3 -- regulator with PLC";
-            mSendRingBuffer =
-                new RingBuffer(audio_input_slot_size, gDefaultOutputQueueLength);
+            cout << "Using experimental buffer strategy " << mBufferStrategy
+                 << "-- Regulator with PLC" << endl;
+
             mReceiveRingBuffer =
-                new Regulator(mSampleRate, mNumAudioChansOut, mAudioBitResolution,
-                              mAudioBufferSize, mBufferQueueLength);
+                new Regulator(mNumAudioChansOut, mAudioBitResolution, mAudioBufferSize,
+                              mBufferQueueLength, mBroadcastQueueLength);
             // bufStrategy 3, mBufferQueueLength is in integer msec not packets
 
             mPacketHeader->setBufferRequiresSameSettings(false);  // = asym is default
+
+            if (0 < mBroadcastQueueLength) {
+                mAudioInterface->enableBroadcastOutput();
+            }
+
         } else {
             cout << "Using JitterBuffer strategy " << mBufferStrategy << endl;
             if (0 > mBufferQueueLength) {
@@ -522,7 +527,7 @@ void JackTrip::startProcess(
                          "clientPingToServerStart"
                       << std::endl;
         if (clientPingToServerStart()
-            == -1) {  // if error on server start (-1) we return inmediatly
+            == -1) {  // if error on server start (-1) we return immediately
             stop(QStringLiteral(
                 "Peer Address has to be set if you run in CLIENTTOPINGSERVER mode"));
             return;
@@ -536,7 +541,7 @@ void JackTrip::startProcess(
                 << "  JackTrip:startProcess case SERVERPINGSERVER before serverStart"
                 << std::endl;
         if (serverStart(true)
-            == -1) {  // if error on server start (-1) we return inmediatly
+            == -1) {  // if error on server start (-1) we return immediately
             stop();
             return;
         }
@@ -651,8 +656,7 @@ void JackTrip::onStatTimer()
             << now.toLocal8Bit().constData() << " "
             << getPeerAddress().toLocal8Bit().constData()
             << " send: " << send_io_stat.underruns << "/" << send_io_stat.overflows
-            << " Pull underruns: "
-            << recv_io_stat.underruns  // pullStat->lastPlcUnderruns;
+            << " Glitches: " << recv_io_stat.underruns  // pullStat->lastPlcUnderruns;
 #define INVFLOATFACTOR 0.001
             << "\nPUSH -- SD avg/last: " << setw(5)
             << INVFLOATFACTOR * recv_io_stat.overflows  // pushStat->longTermStdDev;
@@ -679,8 +683,9 @@ void JackTrip::onStatTimer()
             //                     << "/" << recv_io_stat.overflows << " prot: " <<
             //                     pkt_stat.lost << "/"
             //                     << pkt_stat.outOfOrder << "/" << pkt_stat.revived
-            << " \n tot: "
-            << pkt_stat.tot
+            << " \n tot: " << pkt_stat.tot << " \t tol: " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.autoq_corr << " \t dsp (last): " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.autoq_rate
             //                     << " sync: " << recv_io_stat.level << "/"
             //                     << recv_io_stat.buf_inc_underrun << "/"
             //                     << recv_io_stat.buf_inc_compensate << "/"
@@ -826,7 +831,7 @@ void JackTrip::receivedDataTCP()
     mTcpClient.close();  // Close the socket
     // cout << "TCP Socket Closed!" << endl;
 
-    // If we sent authentication data, check if our authentication attempt was succesfull
+    // If we sent authentication data, check if our authentication attempt was successful
     if (mUseAuth && udp_port > 65535) {
         QString error_message;
         if (udp_port == Auth::WRONGCREDS) {
@@ -854,7 +859,7 @@ void JackTrip::receivedDataTCP()
     }
 
     if (gVerboseFlag)
-        cout << "Connection Succesfull!" << endl;
+        cout << "Connection Successful!" << endl;
 
     // Set with the received UDP port
     // ------------------------------
@@ -967,7 +972,7 @@ void JackTrip::receivedDataUDP()
     //     We reply to the same port the peer sent the packets from
     //     This way we can go through NAT
     //     Because of the NAT traversal scheme, the portn need to be
-    //     "symetric", e.g.:
+    //     "symmetric", e.g.:
     //     from Client to Server : src = 4474, dest = 4464
     //     from Server to Client : src = 4464, dest = 4474
     // no -- all are the same -- 4464
@@ -1116,6 +1121,7 @@ int JackTrip::serverStart(bool timeout, int udpTimeout)  // udpTimeout unused
             mEndTime = udpTimeout;
         }
         mTimeoutTimer.setInterval(mSleepTime);
+        mTimeoutTimer.disconnect();
         connect(&mTimeoutTimer, &QTimer::timeout, this, &JackTrip::udpTimerTick);
         mTimeoutTimer.start();
     }
@@ -1210,6 +1216,7 @@ int JackTrip::clientPingToServerStart()
         mElapsedTime = 0;
         mEndTime     = 5000;  // Timeout after 5 seconds.
         mTimeoutTimer.setInterval(mSleepTime);
+        mTimeoutTimer.disconnect();
         connect(&mTimeoutTimer, &QTimer::timeout, this, &JackTrip::tcpTimerTick);
         mTimeoutTimer.start();
     }
@@ -1297,7 +1304,7 @@ active address local_addr.sin_port = htons(bind_port); //set bind port
   ::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
 #endif
 #if defined ( __APPLE__ )
-  // This option is not avialable on Linux, and without it MAC OS X
+  // This option is not available on Linux, and without it MAC OS X
   // has problems rebinding a socket
   ::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
 #endif
index 0c2ec258a427b190962ff2ddc7358abf190188df..fa877d08ccf960a4a28c661d9bc3f462bc2b8118 100644 (file)
@@ -399,6 +399,12 @@ class JackTrip : public QObject
     {
         return mReceiveRingBuffer->insertSlotNonBlocking(ptrToSlot, len, lostLen);
     }
+    virtual bool writeAudioBufferRegulator(const int8_t* ptrToSlot, int len, int seq,
+                                           int lostLen)
+    {
+        return mReceiveRingBuffer->insertSlotNonBlockingRegulator(ptrToSlot, len, seq,
+                                                                  lostLen);
+    }
     uint32_t getBufferSizeInSamples() const
     {
         return mAudioBufferSize; /*return mAudioInterface->getBufferSizeInSamples();*/
@@ -597,8 +603,8 @@ class JackTrip : public QObject
     /// \brief Starts for the CLIENT mode
     void clientStart();
     /// \brief Starts for the SERVER mode
-    /// \param timout Set the server to timeout after 2 seconds if no client connections
-    /// are received. Usefull for the multithreaded server \return 0 on success, -1 on
+    /// \param timeout Set the server to timeout after 2 seconds if no client connections
+    /// are received. Useful for the multithreaded server \return 0 on success, -1 on
     /// error
     int serverStart(bool timeout = false, int udpTimeout = gTimeOutMultiThreadedServer);
     /// \brief Stats for the Client to Ping Server
index 18a81279e258fe20622db5ea256ca22e533e868f..7ce1d9e5cf64e76f1f89a0bb18f1542e5a280153 100644 (file)
@@ -152,10 +152,10 @@ class JackTripWorker : public QObject
     UdpHubListener* mUdpHubListener;  ///< Hub Listener Socket
     // QHostAddress mClientAddress; ///< Client Address
     QString mClientAddress;
-    uint16_t mServerPort;  ///< Server Ephemeral Incomming Port to use with Client
+    uint16_t mServerPort;  ///< Server Ephemeral Incoming Port to use with Client
     bool m_connectDefaultAudioPorts = false;
 
-    /// Client Outgoing Port. By convention, the receving port will be <tt>mClientPort
+    /// Client Outgoing Port. By convention, the receiving port will be <tt>mClientPort
     /// -1</tt>
     uint16_t mClientPort;
 
index 80213f7f611fb3902d63b53ab41e854617cb14ab..b23b5f088d82a524047ac6035b267ca989c9b609 100644 (file)
@@ -38,8 +38,6 @@
 
 #include "Limiter.h"
 
-#include <iostream>
-
 #include "jacktrip_types.h"
 
 //*******************************************************************************
index f31130847fc065d6db8084e302b6a84b5613cc0c..fea43bd8b70d986aea2bc00fbdf495a5a5b0656e 100644 (file)
@@ -49,6 +49,7 @@
 #endif
 
 #include <cassert>
+#include <iostream>
 #include <vector>
 
 #include "ProcessPlugin.h"
index cbd3bdf2dc7b21350a6098b767daf4f59305dc15..23011e7e12fd19b67e63cbf51d6e290875bed04c 100644 (file)
@@ -297,7 +297,7 @@ void JamLinkHeader::fillHeaderCommonFromAudio()
         // one channel"
         //           << endl;
         // std::exit(1);
-        // std::cerr << "WARINING: JamLink only support ONE channel. Run JackTrip using
+        // std::cerr << "WARNING: JamLink only support ONE channel. Run JackTrip using
         // only one channel" << endl; throw std::logic_error("JamLink only support ONE
         // channel. Run JackTrip using only one channel");
         emit signalError(QStringLiteral(
@@ -307,7 +307,7 @@ void JamLinkHeader::fillHeaderCommonFromAudio()
     // Sampling Rate
     int rate_type = mJackTrip->getSampleRateType();
     if (rate_type != AudioInterface::SR48) {
-        // std::cerr << "WARINING: JamLink only support 48kHz for communication with
+        // std::cerr << "WARNING: JamLink only support 48kHz for communication with
         // JackTrip at the moment." << endl; throw std::logic_error("ERROR: JamLink only
         // support 48kHz for communication with JackTrip at the moment.");
         emit signalError(
@@ -318,7 +318,7 @@ void JamLinkHeader::fillHeaderCommonFromAudio()
     // Check Buffer Size
     int buf_size = mJackTrip->getBufferSizeInSamples();
     if (buf_size != 64) {
-        // std::cerr << "WARINING: JamLink only support 64 buffer size for communication
+        // std::cerr << "WARNING: JamLink only support 64 buffer size for communication
         // with JackTrip at the moment." << endl; throw std::logic_error("ERROR: JamLink
         // only support 64 buffer size for communication with JackTrip at the moment.");
         emit signalError(QStringLiteral(
index 30e9480321e83492279f2eaeb944920580dde3cf..c31b4ba86e59f63fc9e01e81571f3b01a72b741e 100644 (file)
@@ -48,7 +48,7 @@
 #include "jacktrip_types.h"
 class JackTrip;  // Forward Declaration
 
-/// \brief Abstract Header Struct, Header Stucts should subclass it
+/// \brief Abstract Header Struct, Header Structs should subclass it
 struct HeaderStruct {
 };
 
index a9c8e51fe4b842ec79d367d4ed540d84bd7cb0aa..24a6c7dadddd8e276559aa57b81aaa74600db9f1 100644 (file)
  * \date May-Sep 2021
  */
 
-// EXPERIMENTAL for testing in JackTrip v1.5.0
-// requires server and client have same FPP
-// runs ok from FPP 16 up to 1024
-// number of in / out channels should be the same
-// mono, stereo and -n3 tested fine
-
-// ./jacktrip -S --udprt -p1 --bufstrategy 3  -I 1 -q10
-// PIPEWIRE_LATENCY=32/48000 ./jacktrip -C <SERV> --udprt --bufstrategy 3 -I 1 -q4
-
+// EXPERIMENTAL for testing in JackTrip v1.5.<n>
+// server and client can have different FPP (tested from FPP 16 to 1024)
+// stress tested by repeatedly starting & stopping across range of FPP's
+// server and client can have different in / out channel count
+// large FPP, for example 512, should not be run with --udprt as PLC audio callback
+// completion gets delayed auto mode -- use -q auto3 or for manual setting of initial
+// mMsecTolerance -- use -q auto<msec> gathers data for 6 sec and then goes full auto
+
+// example WAN test
+// ./jacktrip -S --udprt -p1 --bufstrategy 3 -q auto
+// PIPEWIRE_LATENCY=32/48000 ./jacktrip -C <SERV> --udprt --bufstrategy 3 -q auto
+
+// (mono : mono : mono)
+// ./jacktrip -S -p1 --bufstrategy 3 -q auto3 -u --receivechannels 1 --sendchannels 1
+// --udprt  -I 1
+// ./jacktrip -C <SERV> --receivechannels 1 -u --sendchannels 1 --bufstrategy 3 -q auto3
+// -I 1 --udprt
+
+// latest (mono <: stereo : stereo)
+// ./jacktrip -S -p1 --bufstrategy 3 -q auto3 -u --receivechannels 1 --sendchannels 2
+// --udprt  -I 1
+// ./jacktrip -C <SERV> --receivechannels 2 -u --sendchannels 1 --bufstrategy 3 -q auto3
+// -I 1 --udprt
+
+// example WAN test
 // at 48000 / 32 = 2.667 ms total roundtrip latency
-// local loopback test with 4 terminals running and the following jmess file
+// local loopback test with 4 terminals running and a jmess file
 // jacktrip -S --udprt --nojackportsconnect -q1 --bufstrategy 3
 // jacktrip -C localhost --udprt --nojackportsconnect -q1  --bufstrategy 3
-// use jack_iodelay
-// use jmess -s delay.xml and jmess -c delay.xml
 
 // tested outgoing loss impairments with (replace lo with relevant network interface)
-// sudo tc qdisc add dev lo root netem loss 2%
-// sudo tc qdisc del dev lo root netem loss 2%
+// sudo tc qdisc add dev lo root netem loss 5%
+// sudo tc qdisc del dev lo root netem loss 5%
+// or very revealing
+// sudo tc qdisc add dev lo root netem loss 20%
+// sudo tc qdisc del dev lo root netem loss 20%
 // tested jitter impairments with
-// for wifi
+// wifi simulation
 // sudo tc qdisc add dev lo root netem slot distribution pareto 0.1ms 3.0ms
 // sudo tc qdisc del dev lo root netem slot distribution pareto 0.1ms 3.0ms
-// for wired cmn9
+// ugly wired simulation
 // sudo tc qdisc add dev lo root netem slot distribution pareto 0.2ms 0.3ms
 // sudo tc qdisc del dev lo root netem slot distribution pareto 0.2ms 0.3ms
 
 #include <iomanip>
 #include <sstream>
 
+#include "JitterBuffer.h"
 #include "jacktrip_globals.h"
 using std::cout;
 using std::endl;
 using std::setw;
 
-// constants... tested for now
-constexpr int HIST          = 6;    // at FPP32
+// constants...
+constexpr int HIST          = 4;    // for mono at FPP 16-128, see below for > mono, > 128
 constexpr int ModSeqNumInit = 256;  // bounds on seqnums, 65536 is max in packet header
 constexpr int NumSlotsMax   = 128;  // mNumSlots looped for recent arrivals
 constexpr int LostWindowMax = 32;   // mLostWindow looped for recent arrivals
+constexpr double DefaultAutoHeadroom =
+    3.0;                           // msec padding for auto adjusting mMsecTolerance
+constexpr double AutoMax = 250.0;  // msec bounds on insane IPI, like ethernet unplugged
+constexpr double AutoInitDur = 6000.0;  // kick in auto after this many msec
+constexpr double AutoInitValFactor =
+    0.5;  // scale for initial mMsecTolerance during init phase if unspecified
+// tweak
+constexpr int WindowDivisor = 8;     // for faster auto tracking
+constexpr int MaxFPP        = 1024;  // tested up to this FPP
 //*******************************************************************************
-Regulator::Regulator(int sample_rate, int channels, int bit_res, int FPP, int qLen)
+Regulator::Regulator(int rcvChannels, int bit_res, int FPP, int qLen, int bqLen)
     : RingBuffer(0, 0)
-    , mNumChannels(channels)
+    , mNumChannels(rcvChannels)
     , mAudioBitRes(bit_res)
     , mFPP(FPP)
-    , mSampleRate(sample_rate)
-    , mMsecTolerance((double)qLen)
+    , mMsecTolerance((double)qLen)  // handle non-auto mode, expects positive qLen
     , mAuto(false)
+    , m_b_BroadcastQueueLength(bqLen)
 {
-    if (mMsecTolerance < 0.0) {  // handle, for example, CLI -q auto15 or -q auto
-        mAuto = true;
-        mMsecTolerance *= -1.0;
-    };
+    // catch settings that are compute bound using long HIST
+    // hub client rcvChannels is set from client's settings parameters
+    // hub server rcvChannels is set from connecting client, not from hub parameters
+    //    if (mNumChannels > MaxChans) {
+    //        std::cerr << "*** Regulator.cpp: receive channels = " << mNumChannels
+    //                  << " larger than max channels = " << MaxChans << "\n";
+    //        exit(1);
+    //    }
+    if (mFPP > MaxFPP) {
+        std::cerr << "*** Regulator.cpp: local FPP = " << mFPP
+                  << " larger than max FPP = " << MaxFPP << "\n";
+        exit(1);
+    }
     switch (mAudioBitRes) {  // int from JitterBuffer to AudioInterface enum
     case 1:
         mBitResolutionMode = AudioInterface::audioBitResolutionT::BIT8;
@@ -105,13 +141,18 @@ Regulator::Regulator(int sample_rate, int channels, int bit_res, int FPP, int qL
         mBitResolutionMode = AudioInterface::audioBitResolutionT::BIT32;
         break;
     }
-    mHist            = HIST * 32;             // samples, from original settings
-    double histFloat = mHist / (double)mFPP;  // packets for other FPP
-    mHist            = (int)histFloat;
-    if (mHist < 2)
-        mHist = 2;  // min packets for prediction, needs at least 2
-    else if (mHist > 6)
-        mHist = 6;  // max packets, keep a lid on CPU load
+    mHist = HIST;  //    HIST (default) is 4
+                   //    as FPP decreases the rate of PLC triggers potentially goes up
+                   //    and load increases so don't use an inverse relation
+
+    //    crossfaded prediction is a full packet ahead of predicted
+    //    packet, so the size of mPrediction needs to account for 2 full packets (2*FPP)
+    //    but trainSamps = (HIST * FPP) and mPrediction.resize(trainSamps - 1, 0.0) so if
+    //    hist = 2, then it exceeds the size
+
+    if (((mNumChannels > 1) && (mFPP > 64)) || (mFPP > 128))
+        mHist = 3;  // min packets for prediction, needs at least 3
+
     if (gVerboseFlag)
         cout << "mHist = " << mHist << " at " << mFPP << "\n";
     mBytes     = mFPP * mNumChannels * mBitResolutionMode;
@@ -124,10 +165,7 @@ Regulator::Regulator(int sample_rate, int channels, int bit_res, int FPP, int qL
         mFadeDown[i] = 1.0 - mFadeUp[i];
     }
     mLastWasGlitch = false;
-    mPacketDurMsec = 1000.0 * (double)mFPP / (double)mSampleRate;
-    if (mMsecTolerance < mPacketDurMsec)
-        mMsecTolerance = mPacketDurMsec;  // absolute minimum
-    mNumSlots = NumSlotsMax;  //((int)ceil(mMsecTolerance / mPacketDurMsec)) + PADSLOTS;
+    mNumSlots      = NumSlotsMax;
 
     for (int i = 0; i < mNumSlots; i++) {
         int8_t* tmp = new int8_t[mBytes];
@@ -143,8 +181,6 @@ Regulator::Regulator(int sample_rate, int channels, int bit_res, int FPP, int qL
     memcpy(mZeros, mXfrBuffer, mBytes);
     mAssembledPacket = new int8_t[mBytes];  // for asym
     memcpy(mAssembledPacket, mXfrBuffer, mBytes);
-    pushStat       = new StdDev(&mIncomingTimer, (int)(floor(48000.0 / (double)mFPP)), 1);
-    pullStat       = new StdDev(&mIncomingTimer, (int)(floor(48000.0 / (double)mFPP)), 2);
     mLastLostCount = 0;  // for stats
     mIncomingTimer.start();
     mLastSeqNumIn  = -1;
@@ -156,23 +192,27 @@ Regulator::Regulator(int sample_rate, int channels, int bit_res, int FPP, int qL
     mModSeqNum           = mNumSlots * 2;
     mFPPratioNumerator   = 1;
     mFPPratioDenominator = 1;
-    mPartialPacketCnt    = 0;
     mFPPratioIsSet       = false;
     mBytesPeerPacket     = mBytes;
-#ifdef GUIBS3
-    // hg for GUI
-    hg = new HerlperGUI(qApp->activeWindow());
-    connect(hg, SIGNAL(moved(double)), this, SLOT(changeGlobal(double)));
-    connect(hg, SIGNAL(moved_2(int)), this, SLOT(changeGlobal_2(int)));
-    connect(hg, SIGNAL(moved_3(int)), this, SLOT(changeGlobal_3(int)));
-#endif
+    mAssemblyCnt         = 0;
+    mModCycle            = 1;
+    mModSeqNumPeer       = 1;
+    mPeerFPP             = mFPP;  // use local until first packet arrives
+    mAutoHeadroom        = DefaultAutoHeadroom;
+    mFPPdurMsec          = 1000.0 * mFPP / 48000.0;
     changeGlobal_3(LostWindowMax);
     changeGlobal_2(NumSlotsMax);  // need hg if running GUI
-    changeGlobal((double)qLen);
+    if (m_b_BroadcastQueueLength) {
+        m_b_ReceiveRingBuffer = new JitterBuffer(
+            mFPP, qLen, 48000, 1, m_b_BroadcastQueueLength, mNumChannels, mAudioBitRes);
+        qDebug() << "Broadcast started in Regulator with packet queue of"
+                 << m_b_BroadcastQueueLength;
+        // have not implemented the mJackTrip->queueLengthChanged functionality
+    }
 }
 
 void Regulator::changeGlobal(double x)
-{  // mMsecTolerance
+{
     mMsecTolerance = x;
     printParams();
 }
@@ -194,75 +234,112 @@ void Regulator::changeGlobal_3(int x)
     printParams();
 }
 
-void Regulator::printParams()
-{
-//    qDebug() << "mMsecTolerance" << mMsecTolerance << "mNumSlots" << mNumSlots
-//             << "mModSeqNum" << mModSeqNum << "mLostWindow" << mLostWindow;
-#ifdef GUIBS3
-    updateGUI((int)mMsecTolerance, mNumSlots);
-#endif
+void Regulator::printParams(){
+    //    qDebug() << "mMsecTolerance" << mMsecTolerance << "mNumSlots" << mNumSlots
+    //             << "mModSeqNum" << mModSeqNum << "mLostWindow" << mLostWindow;
 };
 
-#ifdef GUIBS3
-void Regulator::updateGUI(double msTol, int nSlots)
-{
-    hg->updateDisplay(msTol, nSlots, 0);  // need to remove last param
-}
-#endif
-
 Regulator::~Regulator()
 {
-    delete mXfrBuffer;
-    delete mZeros;
+    delete[] mXfrBuffer;
+    delete[] mZeros;
+    delete[] mAssembledPacket;
+    delete pushStat;
+    delete pullStat;
     for (int i = 0; i < mNumChannels; i++)
         delete mChanData[i];
+    for (auto& slot : mSlots) {
+        delete[] slot;
+    };
+    if (m_b_BroadcastQueueLength)
+        delete m_b_ReceiveRingBuffer;
 }
 
-void Regulator::setFPPratio(int len)
+void Regulator::setFPPratio()
 {
-    int peerFPP = len / (mNumChannels * mBitResolutionMode);
-    if (peerFPP != mFPP) {
-        if (peerFPP > mFPP)
-            mFPPratioDenominator = peerFPP / mFPP;
+    if (mPeerFPP != mFPP) {
+        if (mPeerFPP > mFPP)
+            mFPPratioDenominator = mPeerFPP / mFPP;
         else
-            mFPPratioNumerator = mFPP / peerFPP;
-        qDebug() << "peerBuffers / localBuffers" << mFPPratioNumerator << " / "
-                 << mFPPratioDenominator;
+            mFPPratioNumerator = mFPP / mPeerFPP;
+        //        qDebug() << "peerBuffers / localBuffers" << mFPPratioNumerator << " / "
+        //                 << mFPPratioDenominator;
     }
-    if (mFPPratioNumerator > 1)
+    if (mFPPratioNumerator > 1) {
         mBytesPeerPacket = mBytes / mFPPratioNumerator;
-    mFPPratioIsSet = true;
+        mModCycle        = mFPPratioNumerator - 1;
+        mModSeqNumPeer   = mModSeqNum * mFPPratioNumerator;
+    } else if (mFPPratioDenominator > 1) {
+        mModSeqNumPeer = mModSeqNum / mFPPratioDenominator;
+    }
 }
 
 //*******************************************************************************
 void Regulator::shimFPP(const int8_t* buf, int len, int seq_num)
 {
     if (seq_num != -1) {
-        if (!mFPPratioIsSet)
-            setFPPratio(len);
-        if (mFPPratioNumerator > 1) {  // 2/1, 4/1 peer FPP is lower
-            int modSeqNumPeer = mModSeqNum * mFPPratioNumerator;
-            seq_num %= modSeqNumPeer;
-            //        qDebug() << seq_num << seq_num / mFPPratioNumerator <<
-            //        mPartialPacketCnt;
-            seq_num /= mFPPratioNumerator;
-            int tmp = (mPartialPacketCnt % mFPPratioNumerator) * mBytesPeerPacket;
-            memcpy(&mAssembledPacket[tmp], buf, mBytesPeerPacket);
-            if ((mPartialPacketCnt % mFPPratioNumerator) == (mFPPratioNumerator - 1))
-                pushPacket(mAssembledPacket, seq_num);
-            mPartialPacketCnt++;
-        } else if (mFPPratioDenominator > 1) {  // 1/2, 1/4 peer FPP is higher
-            int modSeqNumPeer = mModSeqNum / mFPPratioDenominator;
-            seq_num %= modSeqNumPeer;
-            seq_num *= mFPPratioDenominator;
-            for (int i = 0; i < mFPPratioDenominator; i++) {
-                int tmp = i * mBytes;
-                memcpy(mAssembledPacket, &buf[tmp], mBytes);
-                pushPacket(mAssembledPacket, seq_num);
-                seq_num++;
-            }
-        } else
+        if (!mFPPratioIsSet) {  // first peer packet
+            mPeerFPP = len / (mNumChannels * mBitResolutionMode);
+            // bufstrategy 1 autoq mode overloads qLen with negative val
+            // creates this ugly code
+            if (mMsecTolerance < 0) {  // handle -q auto or, for example, -q auto10
+                mAuto = true;
+                // default is -500 from bufstrategy 1 autoq mode
+                // tweak
+                if (mMsecTolerance != -500.0) {
+                    // use it to set headroom
+                    mAutoHeadroom = -mMsecTolerance;
+                    qDebug() << "PLC is in auto mode and has been set with"
+                             << mAutoHeadroom << "ms headroom";
+                    if (mAutoHeadroom > 50.0)
+                        qDebug() << "That's a very large value and should be less than, "
+                                    "for example, 50ms";
+                }
+                // found an interesting relationship between mPeerFPP and initial
+                // mMsecTolerance mPeerFPP*0.5 is pretty good though that's an oddball
+                // conversion of bufsize directly to msec
+                mMsecTolerance = (mPeerFPP * AutoInitValFactor);
+            };
+            setFPPratio();
+            // number of stats tick calls per sec depends on FPP
+            int maxFPP = (mPeerFPP > mFPP) ? mPeerFPP : mFPP;
+            pushStat =
+                new StdDev(1, &mIncomingTimer, (int)(floor(48000.0 / (double)maxFPP)));
+            pullStat =
+                new StdDev(2, &mIncomingTimer, (int)(floor(48000.0 / (double)mFPP)));
+            mFPPratioIsSet = true;
+        }
+        if (mFPPratioNumerator == mFPPratioDenominator) {
             pushPacket(buf, seq_num);
+        } else {
+            seq_num %= mModSeqNumPeer;
+            if (mFPPratioNumerator > 1) {  // 2/1, 4/1 peer FPP is lower, , (local/peer)/1
+                int tmp = (seq_num % mFPPratioNumerator) * mBytesPeerPacket;
+                memcpy(&mAssembledPacket[tmp], buf, mBytesPeerPacket);
+                if ((seq_num % mFPPratioNumerator) == mModCycle) {
+                    if (mAssemblyCnt == mModCycle)
+                        pushPacket(mAssembledPacket, seq_num / mFPPratioNumerator);
+                    //                    else
+                    //                        qDebug() << "incomplete due to lost packet";
+                    mAssemblyCnt = 0;
+                } else
+                    mAssemblyCnt++;
+            } else if (mFPPratioDenominator
+                       > 1) {  // 1/2, 1/4 peer FPP is higher, 1/(peer/local)
+                seq_num *= mFPPratioDenominator;
+                for (int i = 0; i < mFPPratioDenominator; i++) {
+                    int tmp = i * mBytes;
+                    memcpy(mAssembledPacket, &buf[tmp], mBytes);
+                    pushPacket(mAssembledPacket, seq_num);
+                    seq_num++;
+                }
+            }
+        }
+        pushStat->tick();
+        double adjustAuto = pushStat->calcAuto(mAutoHeadroom, mFPPdurMsec);
+        //        qDebug() << adjustAuto;
+        if (mAuto && (pushStat->lastTime > AutoInitDur))
+            mMsecTolerance = adjustAuto;
     }
 };
 
@@ -270,20 +347,13 @@ void Regulator::shimFPP(const int8_t* buf, int len, int seq_num)
 void Regulator::pushPacket(const int8_t* buf, int seq_num)
 {
     QMutexLocker locker(&mMutex);
-    //    qDebug() << "\t" << seq_num;
     seq_num %= mModSeqNum;
-    // if (seq_num==0) return;   // if (seq_num==1) return; // impose regular loss
+    // if (seq_num==0) return;   // impose regular loss
     mIncomingTiming[seq_num] =
         mMsecTolerance + (double)mIncomingTimer.nsecsElapsed() / 1000000.0;
     mLastSeqNumIn = seq_num;
     if (mLastSeqNumIn != -1)
         memcpy(mSlots[mLastSeqNumIn % mNumSlots], buf, mBytes);
-    double nowMS = pushStat->tick();
-    if (mAuto && (nowMS > 2000.0)) {
-        double tmp = pushStat->longTermStdDev + pushStat->longTermMax;
-        tmp += 2.0;  // 2 ms -- kind of a guess
-        changeGlobal(tmp);
-    }
 };
 
 //*******************************************************************************
@@ -291,7 +361,7 @@ void Regulator::pullPacket(int8_t* buf)
 {
     QMutexLocker locker(&mMutex);
     mSkip = 0;
-    if (mLastSeqNumIn == -1) {
+    if ((mLastSeqNumIn == -1) || (!mFPPratioIsSet)) {
         goto ZERO_OUTPUT;
     } else {
         mLastSeqNumOut++;
@@ -316,16 +386,19 @@ void Regulator::pullPacket(int8_t* buf)
     }
 
 PACKETOK : {
-    if (mSkip)
+    if (mSkip) {
         processPacket(true);
-    else
+        pullStat->plcOverruns += mSkip;
+    } else
         processPacket(false);
+    pullStat->tick();
     goto OUTPUT;
 }
 
 UNDERRUN : {
     processPacket(true);
     pullStat->plcUnderruns++;  // count late
+    pullStat->tick();
     goto OUTPUT;
 }
 
@@ -334,17 +407,28 @@ ZERO_OUTPUT:
 
 OUTPUT:
     memcpy(buf, mXfrBuffer, mBytes);
-    pullStat->tick();
 };
 
 //*******************************************************************************
 void Regulator::processPacket(bool glitch)
 {
+    double tmp = 0.0;
+    if ((glitch) && (mFPPratioDenominator > 1)) {
+        glitch = !(mLastSeqNumOut % mFPPratioDenominator);
+    }
+    if (glitch)
+        tmp = (double)mIncomingTimer.nsecsElapsed();
     for (int ch = 0; ch < mNumChannels; ch++)
         processChannel(ch, glitch, mPacketCnt, mLastWasGlitch);
     mLastWasGlitch = glitch;
     mPacketCnt++;
     // 32 bit is good for days:  (/ (* (- (expt 2 32) 1) (/ 32 48000.0)) (* 60 60 24))
+
+    if (glitch) {
+        double tmp2 = (double)mIncomingTimer.nsecsElapsed() - tmp;
+        tmp2 /= 1000000.0;
+        pullStat->lastPLCdspElapsed = tmp2;
+    }
 }
 
 //*******************************************************************************
@@ -367,9 +451,10 @@ void Regulator::processChannel(int ch, bool glitch, int packetCnt, bool lastWasG
             // LINEAR PREDICT DATA
             cd->mTail = cd->mTrain;
 
-            ba.predict(cd->mCoeffs, cd->mTail);  // resizes to TRAINSAMPS-2 + TRAINSAMPS
+            ba.predict(cd->mCoeffs,
+                       cd->mTail);  // resizes to TRAINSAMPS-2 + TRAINSAMPS
 
-            for (int i = 0; i < (cd->trainSamps - 1); i++)
+            for (int i = 0; i < (cd->trainSamps - 2); i++)
                 cd->mPrediction[i] = cd->mTail[i + cd->trainSamps];
         }
         // cross fade last prediction with mTruth
@@ -450,7 +535,7 @@ bool BurgAlgorithm::classify(double d)
         tmp = true;
         break;
     case FP_ZERO:
-        //      qDebug() <<  ("zero");
+        qDebug() << ("zero");
         tmp = true;
         break;
     case FP_SUBNORMAL:
@@ -470,8 +555,8 @@ void BurgAlgorithm::train(std::vector<long double>& coeffs, const std::vector<fl
     size_t N = x.size() - 1;
     size_t m = coeffs.size();
 
-    //        if (x.size() < m) qDebug() << "time_series should have more elements than
-    //        the AR order is";
+    //        if (x.size() < m) qDebug() << "time_series should have more elements
+    //        than the AR order is";
 
     // INITIALIZE Ak
     //    vector<long double> Ak(m + 1, 0.0);
@@ -575,8 +660,9 @@ ChanData::ChanData(int i, int FPP, int hist) : ch(i)
 }
 
 //*******************************************************************************
-StdDev::StdDev(QElapsedTimer* timer, int w, int id) : mTimer(timer), window(w), mId(id)
+StdDev::StdDev(int id, QElapsedTimer* timer, int w) : mId(id), mTimer(timer), window(w)
 {
+    window /= WindowDivisor;
     reset();
     longTermStdDev    = 0.0;
     longTermStdDevAcc = 0.0;
@@ -587,21 +673,35 @@ StdDev::StdDev(QElapsedTimer* timer, int w, int id) : mTimer(timer), window(w),
     longTermMax       = 0.0;
     longTermMaxAcc    = 0.0;
     lastTime          = 0.0;
+    lastPLCdspElapsed = 0.0;
     data.resize(w, 0.0);
 }
 
 void StdDev::reset()
 {
-    mean = 0.0;
-    //        varRunning = 0.0;
-    acc          = 0.0;
-    min          = 999999.0;
-    max          = 0.0;
     ctr          = 0;
+    plcOverruns  = 0;
     plcUnderruns = 0;
+    mean         = 0.0;
+    acc          = 0.0;
+    min          = 999999.0;
+    max          = -999999.0;
+};
+
+double StdDev::calcAuto(double autoHeadroom, double localFPPdur)
+{
+    //    qDebug() << longTermStdDev << longTermMax << AutoMax << window <<
+    //    longTermCnt;
+    if ((longTermStdDev == 0.0) || (longTermMax == 0.0))
+        return AutoMax;
+    double tmp = longTermStdDev + ((longTermMax > AutoMax) ? AutoMax : longTermMax);
+    if (tmp < localFPPdur)
+        tmp = localFPPdur;  // might also check peerFPP...
+    tmp += autoHeadroom;
+    return tmp;
 };
 
-double StdDev::tick()
+void StdDev::tick()
 {
     double now       = (double)mTimer->nsecsElapsed() / 1000000.0;
     double msElapsed = now - lastTime;
@@ -622,15 +722,15 @@ double StdDev::tick()
             var += (tmp * tmp);
         }
         var /= (double)window;
-        double stdDev = sqrt(var);
+        double stdDevTmp = sqrt(var);
         if (longTermCnt) {
-            longTermStdDevAcc += stdDev;
+            longTermStdDevAcc += stdDevTmp;
             longTermStdDev = longTermStdDevAcc / (double)longTermCnt;
             longTermMaxAcc += max;
             longTermMax = longTermMaxAcc / (double)longTermCnt;
             if (gVerboseFlag)
                 cout << setw(10) << mean << setw(10) << lastMin << setw(10) << max
-                     << setw(10) << stdDev << setw(10) << longTermStdDev << " " << mId
+                     << setw(10) << stdDevTmp << setw(10) << longTermStdDev << " " << mId
                      << endl;
         } else if (gVerboseFlag)
             cout << "printing directly from Regulator->stdDev->tick:\n (mean / min / "
@@ -638,19 +738,21 @@ double StdDev::tick()
                     "stdDev / longTermStdDev) \n";
 
         longTermCnt++;
-        lastMean   = mean;
-        lastMin    = min;
-        lastMax    = max;
-        lastStdDev = stdDev;
+        lastMean         = mean;
+        lastMin          = min;
+        lastMax          = max;
+        lastPlcOverruns  = plcOverruns;
+        lastPlcUnderruns = plcUnderruns;
+        lastStdDev       = stdDevTmp;
         reset();
     }
-    return lastTime;
 }
+
 //*******************************************************************************
 bool Regulator::getStats(RingBuffer::IOStat* stat, bool reset)
 {
     QMutexLocker locker(&mMutex);
-    if (reset) {  // all are unused
+    if (reset) {  // all are unused, this is copied from superclass
         mUnderruns        = 0;
         mOverflows        = 0;
         mSkew0            = mLevel;
@@ -661,68 +763,23 @@ bool Regulator::getStats(RingBuffer::IOStat* stat, bool reset)
         mBufIncCompensate = 0;
         mBroadcastSkew    = 0;
     }
+
     // hijack  of  struct IOStat {
-    stat->underruns = pullStat->lastPlcUnderruns;
+    stat->underruns = pullStat->lastPlcUnderruns + pullStat->lastPlcOverruns;
 #define FLOATFACTOR 1000.0
-    stat->overflows         = FLOATFACTOR * pushStat->longTermStdDev;
-    stat->skew              = FLOATFACTOR * pushStat->lastMean;
-    stat->skew_raw          = FLOATFACTOR * pushStat->lastMin;
-    stat->level             = FLOATFACTOR * pushStat->lastMax;
-    stat->buf_dec_overflows = FLOATFACTOR * pushStat->lastStdDev;
-
+    stat->overflows = FLOATFACTOR * pushStat->longTermStdDev;
+    stat->skew      = FLOATFACTOR * pushStat->lastMean;
+    stat->skew_raw  = FLOATFACTOR * pushStat->lastMin;
+    stat->level     = FLOATFACTOR * pushStat->lastMax;
+    //    stat->level              = FLOATFACTOR * pushStat->longTermMax;
+    stat->buf_dec_overflows  = FLOATFACTOR * pushStat->lastStdDev;
+    stat->autoq_corr         = FLOATFACTOR * mMsecTolerance;
     stat->buf_dec_pktloss    = FLOATFACTOR * pullStat->longTermStdDev;
     stat->buf_inc_underrun   = FLOATFACTOR * pullStat->lastMean;
     stat->buf_inc_compensate = FLOATFACTOR * pullStat->lastMin;
     stat->broadcast_skew     = FLOATFACTOR * pullStat->lastMax;
     stat->broadcast_delta    = FLOATFACTOR * pullStat->lastStdDev;
-    // unused
-    //        int32_t autoq_corr;
-    //        int32_t autoq_rate;
+    stat->autoq_rate         = FLOATFACTOR * pullStat->lastPLCdspElapsed;
+    // none are unused
     return true;
 }
-/*
-QString Regulator::getStats(uint32_t statCount, uint32_t lostCount)
-{
-    // formatting floats in columns looks better with std::stringstream than with
-    // QTextStream
-    QString tmp;
-    if (!statCount) {
-        tmp = QString("Regulator: inter-packet intervals msec\n");
-        tmp += "                 (window of last ";
-        tmp += QString::number(pullStat->window);
-        tmp += " packets)\n";
-        tmp +=
-                "secs   avgStdDev (mean       min       max     stdDev) "
-                "PLC(under over  skipped) lost\n";
-    } else {
-        uint32_t lost  = lostCount - mLastLostCount;
-        mLastLostCount = lostCount;
-#define PDBL(x)  << setw(10) << (QString("%1").arg(pushStat->x, 0, 'f', 2)).toStdString()
-#define PDBL2(x) << setw(10) << (QString("%1").arg(pullStat->x, 0, 'f', 2)).toStdString()
-        std::stringstream logger;
-        logger << setw(2)
-               << statCount
-                  PDBL(longTermStdDev) PDBL(lastMean) PDBL(lastMin) PDBL(lastMax)
-PDBL(lastStdDev)
-               << setw(8) << pushStat->lastPlcSkipped
-          #ifndef GUIBS3
-                  // comment out this next line for GUI because...
-               << endl
-                  // ...print all stats in one line when running in GUI because can't
-handle extra crlf
-                  // and... to actually see the two lines, need to run it in terminal
-          #endif
-                  ;
-        tmp = QString::fromStdString(logger.str());
-        std::stringstream logger2;
-        logger2 << setw(2)
-                << "" PDBL2(longTermStdDev) PDBL2(lastMean) PDBL2(lastMin) PDBL2(lastMax)
-                   PDBL2(lastStdDev)
-                << setw(8) << pullStat->lastPlcUnderruns << setw(8)
-                << pullStat->lastPlcOverruns << setw(8) << pullStat->lastPlcSkipped
-                << setw(8) << lost << endl;
-        tmp += QString::fromStdString(logger2.str());
-    }
-    return tmp;
-}
-*/
index 96d4e0efa4ea80c36d37c1bd0f34d575914d1367..9f66afc5087eb132117f7ff02ac00dfbd9f15867 100644 (file)
@@ -35,8 +35,7 @@
  * \date May 2021
  */
 
-// EXPERIMENTAL for testing in JackTrip v1.4.0
-// Initial references and starter code
+// Initial references and starter code to bring up Burg's recursion
 // http://www.emptyloop.com/technotes/A%20tutorial%20on%20Burg's%20method,%20algorithm%20and%20recursion.pdf
 // https://metacpan.org/source/SYP/Algorithm-Burg-0.001/README
 
 #include "AudioInterface.h"
 #include "RingBuffer.h"
 
-//#define GUIBS3
-#ifdef GUIBS3
-#include <QWidget>
-
-#include "herlpergui.h"
-#include "ui_herlpergui.h"
-#endif
-
 class BurgAlgorithm
 {
    public:
@@ -95,46 +86,42 @@ class ChanData
 class StdDev
 {
    public:
-    StdDev(QElapsedTimer* timer, int w, int id);
-    void reset();
-    double tick();
-    QElapsedTimer* mTimer;
-    std::vector<double> data;
-    double mean;
-    double var;
-    //    double varRunning;
-    int window;
+    StdDev(int id, QElapsedTimer* timer, int w);
+    void tick();
+    double calcAuto(double autoHeadroom, double localFPPdur);
     int mId;
-    double acc;
-    double min;
-    double max;
-    int ctr;
+    int plcOverruns;
+    int plcUnderruns;
+    double lastTime;
     double lastMean;
     double lastMin;
     double lastMax;
-    int plcUnderruns;
+    int lastPlcOverruns;
     int lastPlcUnderruns;
+    double lastPLCdspElapsed;
     double lastStdDev;
     double longTermStdDev;
     double longTermStdDevAcc;
     double longTermMax;
     double longTermMaxAcc;
-    double lastTime;
+
+   private:
+    void reset();
+    QElapsedTimer* mTimer;
+    std::vector<double> data;
+    double mean;
+    int window;
+    double acc;
+    double min;
+    double max;
+    int ctr;
     int longTermCnt;
 };
 
-#ifdef GUIBS3
-class Regulator
-    : public QObject
-    , public RingBuffer
-{
-    Q_OBJECT;
-#else
 class Regulator : public RingBuffer
 {
-#endif
    public:
-    Regulator(int sample_rate, int channels, int bit_res, int FPP, int qLen);
+    Regulator(int rcvChannels, int bit_res, int FPP, int qLen, int bqLen);
     virtual ~Regulator();
 
     void shimFPP(const int8_t* buf, int len, int seq_num);
@@ -142,31 +129,39 @@ class Regulator : public RingBuffer
     // can hijack unused2 to propagate incoming seq num if needed
     // option is in UdpDataProtocol
     // if (!mJackTrip->writeAudioBuffer(src, host_buf_size, last_seq_num))
-    // instread of
+    // instead of
     // if (!mJackTrip->writeAudioBuffer(src, host_buf_size, gap_size))
-    virtual bool insertSlotNonBlocking(const int8_t* ptrToSlot, [[maybe_unused]] int len,
-                                       [[maybe_unused]] int seq_num)
+    virtual bool insertSlotNonBlockingRegulator(const int8_t* ptrToSlot,
+                                                [[maybe_unused]] int len,
+                                                [[maybe_unused]] int seq_num, int lostLen)
     {
         shimFPP(ptrToSlot, len, seq_num);
+        if (m_b_BroadcastQueueLength)
+            m_b_ReceiveRingBuffer->insertSlotNonBlocking(ptrToSlot, len, lostLen);
         return (true);
     }
 
     void pullPacket(int8_t* buf);
 
     virtual void readSlotNonBlocking(int8_t* ptrToReadSlot) { pullPacket(ptrToReadSlot); }
+    virtual void readBroadcastSlot(int8_t* ptrToReadSlot)
+    {
+        m_b_ReceiveRingBuffer->readSlotNonBlocking(ptrToReadSlot);
+        m_b_ReceiveRingBuffer->readBroadcastSlot(ptrToReadSlot);
+    }
 
     //    virtual QString getStats(uint32_t statCount, uint32_t lostCount);
     virtual bool getStats(IOStat* stat, bool reset);
 
    private:
-    void setFPPratio(int len);
+    void setFPPratio();
     bool mFPPratioIsSet;
     void processPacket(bool glitch);
     void processChannel(int ch, bool glitch, int packetCnt, bool lastWasGlitch);
     int mNumChannels;
     int mAudioBitRes;
     int mFPP;
-    int mSampleRate;
+    int mPeerFPP;
     uint32_t mLastLostCount;
     int mNumSlots;
     int mHist;
@@ -191,7 +186,6 @@ class Regulator : public RingBuffer
     QElapsedTimer mIncomingTimer;
     int mLastSeqNumIn;
     int mLastSeqNumOut;
-    double mPacketDurMsec;
     std::vector<double> mPhasor;
     std::vector<double> mIncomingTiming;
     int mModSeqNum;
@@ -199,16 +193,18 @@ class Regulator : public RingBuffer
     int mSkip;
     int mFPPratioNumerator;
     int mFPPratioDenominator;
-    int mPartialPacketCnt;
+    int mAssemblyCnt;
+    int mModCycle;
     bool mAuto;
-#ifdef GUIBS3
-    HerlperGUI* hg;
-    void updateGUI(double msTol, int nSlots, int lostWin);
-   public slots:
-#endif
+    int mModSeqNumPeer;
+    double mAutoHeadroom;
+    double mFPPdurMsec;
     void changeGlobal(double);
     void changeGlobal_2(int);
     void changeGlobal_3(int);
     void printParams();
+    /// Pointer for the Receive RingBuffer
+    RingBuffer* m_b_ReceiveRingBuffer;
+    int m_b_BroadcastQueueLength;
 };
 #endif  //__REGULATOR_H__
index 977fcbd06bc49386770bfc23c78b961edf00b165..3f1774d683cdce3c1a88d5e9fe8c963015124737 100644 (file)
@@ -37,8 +37,6 @@
 
 #include "Reverb.h"
 
-#include <iostream>
-
 #include "jacktrip_types.h"
 
 //*******************************************************************************
index 34646601a188ddcbe82a9c31d166e6cc3ad851e0..8970e0818b9a551f346f470c63b03fd8ab10dd9e 100644 (file)
@@ -41,6 +41,8 @@
 #ifndef __REVERB_H__
 #define __REVERB_H__
 
+#include <iostream>
+
 //#define SINE_TEST
 
 #include "ProcessPlugin.h"
index 9e5ffc794fe8fee0d19687cdd3204ef82aa36caa..8e362bb5e4970a8bc42990f7f1bb6bd6a61c986f 100644 (file)
@@ -69,7 +69,7 @@ RingBuffer::RingBuffer(int SlotSize, int NumSlots)
     }
 
     // Advance write position to half of the RingBuffer
-    // Udpate Full Slots accordingly
+    // Update Full Slots accordingly
     mFullSlots        = (NumSlots / 2);
     mLevelDownRate    = 0.01;
     mStatUnit         = 1;
@@ -150,7 +150,7 @@ void RingBuffer::readSlotBlocking(int8_t* ptrToReadSlot)
 bool RingBuffer::insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen)
 {
     if (len != mSlotSize && 0 != len) {
-        // RingBuffer does not suppport mixed buf sizes
+        // RingBuffer does not support mixed buf sizes
         return false;
     }
     QMutexLocker locker(&mMutex);  // lock the mutex
@@ -168,7 +168,7 @@ bool RingBuffer::insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int los
     /// \todo It may be better here to insert the slot anyways,
     /// instead of not writing anything
     if (mFullSlots == mNumSlots) {
-        // std::cout << "OUPUT OVERFLOW NON BLOCKING = " << mNumSlots << std::endl;
+        // std::cout << "OUTPUT OVERFLOW NON BLOCKING = " << mNumSlots << std::endl;
         overflowReset();
         return true;
     }
@@ -183,6 +183,15 @@ bool RingBuffer::insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int los
     return true;
 }
 
+//*******************************************************************************
+bool RingBuffer::insertSlotNonBlockingRegulator([[maybe_unused]] const int8_t* ptrToSlot,
+                                                [[maybe_unused]] int len,
+                                                [[maybe_unused]] int seq_num,
+                                                [[maybe_unused]] int lostLen)
+{
+    return true;
+}
+
 //*******************************************************************************
 void RingBuffer::readSlotNonBlocking(int8_t* ptrToReadSlot)
 {
index b5175118e27777863777d331720fd4dfcbd59fef..f7415cba59fc24885b6d9d8e703e6f6add1c37fd 100644 (file)
@@ -94,6 +94,12 @@ class RingBuffer
      */
     virtual bool insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen);
 
+    /** \brief Same as insertSlotNonBlocking but seq_num for Regulator
+     * \param ptrToSlot Pointer to slot to insert into the RingBuffer
+     */
+    virtual bool insertSlotNonBlockingRegulator(const int8_t* ptrToSlot, int len,
+                                                int seq_num, int lostLen);
+
     /** \brief Same as readSlotBlocking but non-blocking (asynchronous)
      * \param ptrToReadSlot Pointer to read slot from the RingBuffer
      */
index 1797d3264e868c17fec99fc3da1e8adc06593558..a40c1e1f9ee0ebaf9f81680ee9427645f03718c5 100644 (file)
@@ -155,7 +155,7 @@ void RtAudioInterface::setup()
 
     try {
         // IMPORTANT NOTE: It's VERY important to remember to pass this
-        // as the user data in the process callback, otherwise memeber won't
+        // as the user data in the process callback, otherwise member won't
         // be accessible
         mRtAudio->openStream(&out_params, &in_params, RTAUDIO_FLOAT32, sampleRate,
                              &bufferFrames, &RtAudioInterface::wrapperRtAudioCallback,
@@ -268,7 +268,7 @@ void RtAudioInterface::printDeviceInfo(unsigned int deviceId)
         cout << "  --Default Output Device--" << endl;
     }
     if (info.isDefaultInput) {
-        cout << "  --Default Intput Device--" << endl;
+        cout << "  --Default Input Device--" << endl;
     }
     if (info.probed) {
         cout << "  --Probed Successful--" << endl;
index 5915ad4272851e58ed80942d6d9fe04f65512ae2..51b6e9de2e0817b3ebd88e3193099c017903dc83 100644 (file)
@@ -60,7 +60,7 @@ class RtAudioInterface : public AudioInterface
     /// \brief The class destructor
     virtual ~RtAudioInterface();
 
-    /// \brief List all avialable audio interfaces, with its properties
+    /// \brief List all available audio interfaces, with its properties
     virtual void listAllInterfaces();
     static void printDevices();
     virtual int getDeviceIdFromName(std::string deviceName, bool isInput);
index 4084c067f4f42ce5c8a51f534a382d54c47218cf..e964815c6bcd9f264c4e6446241f9210d74993dc 100644 (file)
@@ -1015,7 +1015,7 @@ JackTrip* Settings::getConfiguredJackTrip()
         jackTrip->setSampleRate(mSampleRate);
     }
 
-    // Change defualt device ID
+    // Change default device ID
     if (mChangeDefaultID) {
         jackTrip->setDeviceID(mDeviceID);
     }
index 6f61ad12b1af5fa6508695d68098f7c12abd43b4..43d5df8e1faa9ff4b2e47975fb08069b0ddab68b 100644 (file)
@@ -52,7 +52,7 @@
 //#include <winsock.h>
 #include <winsock2.h>  //cc need SD_SEND
 #else
-#include <sys/fcntl.h>
+#include <fcntl.h>
 #include <sys/socket.h>  // for POSIX Sockets
 #include <unistd.h>
 #ifndef MANUAL_POLL
@@ -217,7 +217,7 @@ int UdpDataProtocol::bindSocket()
 
     err = WSAStartup(wVersionRequested, &wsaData);
     if (err != 0) {
-        // Tell the user that we couldn't find a useable
+        // Tell the user that we couldn't find a usable
         // winsock.dll.
 
         return INVALID_SOCKET;
@@ -226,7 +226,7 @@ int UdpDataProtocol::bindSocket()
     // Confirm that the Windows Sockets DLL supports 1.1. or higher
 
     if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
-        // Tell the user that we couldn't find a useable
+        // Tell the user that we couldn't find a usable
         // winsock.dll.
         WSACleanup();
         return INVALID_SOCKET;
@@ -267,11 +267,36 @@ int UdpDataProtocol::bindSocket()
 #elif defined(__linux__)
     ::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
 #else
-    // This option is not avialable on Linux, and without it MAC OS X
+    // This option is not available on Linux, and without it MAC OS X
     // has problems rebinding a socket
     ::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
 #endif
 
+#if defined(_WIN32)
+    // TODO: these don't seem to work on windows. we likely need to use qWAVE or qos2
+#elif defined(__APPLE__)
+    // set service type "Interactive Voice"
+    // TODO: this is supposed to be the right thing to do on OSX, but doesn't seem to do
+    // anything
+    const int val = NET_SERVICE_TYPE_VO;
+    ::setsockopt(sock_fd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &val, sizeof(val));
+#else
+    // Set ToS to DSCP Expedited Forwarding (EF), recommended for Audio
+    // See RFC2474 https://datatracker.ietf.org/doc/html/rfc2474
+    // See also
+    // https://www.slashroot.in/understanding-differentiated-services-tos-field-internet-protocol-header
+    const char tos = 0xB8;  // 10111000
+    if (mIPv6) {
+        ::setsockopt(sock_fd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos));
+    } else {
+        ::setsockopt(sock_fd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos));
+    }
+
+    // Set 802.1q QoS priority
+    int priority = 6;
+    ::setsockopt(sock_fd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority));
+#endif
+
     // Bind the Socket
     if (mIPv6) {
         if ((::bind(sock_fd, (struct sockaddr*)&local_addr6, sizeof(local_addr6))) < 0) {
@@ -690,7 +715,7 @@ void UdpDataProtocol::waitForReady(int timeout_msec)
     int loop_resolution_usec = 100;    // usecs to wait on each loop
     int emit_resolution_usec = 10000;  // 10 milliseconds
     int timeout_usec         = timeout_msec * 1000;
-    int elapsed_time_usec    = 0;  // Ellapsed time in milliseconds
+    int elapsed_time_usec    = 0;  // Elapsed time in milliseconds
 
     while (!datagramAvailable() && (elapsed_time_usec <= timeout_usec) && !mStopped) {
         //    if (mStopped) { return false; }
@@ -811,7 +836,7 @@ void UdpDataProtocol::receivePacketRedundancy(
         int ok = true; // send audio buf to
         ok = (mJackTrip->getBufferStrategy() !=3) ? // ring or jitter
                     mJackTrip->writeAudioBuffer(src, host_buf_size, gap_size)
-                  : mJackTrip->writeAudioBuffer(src, host_buf_size, last_seq_num);
+                  : mJackTrip->writeAudioBufferRegulator(src, host_buf_size, last_seq_num, gap_size);
         if (!ok) {
             emit signalError("Local and Peer buffer settings are incompatible");
             cout << "ERROR: Local and Peer buffer settings are incompatible" << endl;
@@ -879,7 +904,7 @@ void UdpDataProtocol::sendPacketRedundancy(int8_t* full_redundant_packet,
     // Move older packets to end of array of redundant packets
     std::memmove(full_redundant_packet + full_packet_size, full_redundant_packet,
                  full_packet_size * (mUdpRedundancyFactor - 1));
-    // Copy new packet to the begining of array
+    // Copy new packet to the beginning of array
     std::memcpy(full_redundant_packet, mFullPacket, full_packet_size);
 
     // 10% (or other number) packet lost simulation.
@@ -929,7 +954,7 @@ void UdpDataProtocol::sendPacketRedundancy(int8_t* full_redundant_packet,
   etc...
 
   Then, the receiving end checks if the firs packet in the list is the one it should use,
-  otherwise it continure reding the mUdpRedundancyFactor packets until it finds the one that
+  otherwise it continue reading the mUdpRedundancyFactor packets until it finds the one that
   should come next (this can better perfected by just jumping until the correct packet).
   If it has more than one packet that it hasn't yet received, it sends it to the soundcard
   one by one.
index d437998bee1f9dcdac7e9a7875dee47b07fc208a..d3ffbe66bc4314f3b2aea94d80387855399abdc3 100644 (file)
@@ -61,7 +61,7 @@
  * The SENDER and RECEIVER socket can share the same port/address pair (for compatibility
  * with the JamLink boxes). This is achieved setting
  * the resusable property in the socket for address and port. You have to
- * externaly check if the port is already binded if you want to avoid re-binding to the
+ * externally check if the port is already binded if you want to avoid re-binding to the
  * same port.
  */
 class UdpDataProtocol : public DataProtocol
@@ -98,9 +98,9 @@ class UdpDataProtocol : public DataProtocol
 
     /** \brief Receives a packet. It blocks until a packet is received
      *
-     * This function makes sure we recieve a complete packet
+     * This function makes sure we receive a complete packet
      * of size n
-     * \param buf Buffer to store the recieved packet
+     * \param buf Buffer to store the received packet
      * \param n size of packet to receive
      * \return number of bytes read, -1 on error
      */
@@ -141,7 +141,8 @@ class UdpDataProtocol : public DataProtocol
     /** \brief Implements the Thread Loop. To start the thread, call start()
      * ( DO NOT CALL run() )
      *
-     * This function creats and binds all the socket and start the connection loop thread.
+     * This function creates and binds all the socket and start the connection loop
+     * thread.
      */
     virtual void run();
 
@@ -180,14 +181,14 @@ class UdpDataProtocol : public DataProtocol
      */
     void waitForReady(int timeout_msec);
 
-    /** \brief Redundancy algorythm at the receiving end
+    /** \brief Redundancy algorithm at the receiving end
      */
     virtual void receivePacketRedundancy(int8_t* full_redundant_packet,
                                          int full_redundant_packet_size,
                                          int full_packet_size, uint16_t& current_seq_num,
                                          uint16_t& last_seq_num, uint16_t& newer_seq_num);
 
-    /** \brief Redundancy algorythm at the sender's end
+    /** \brief Redundancy algorithm at the sender's end
      */
     virtual void sendPacketRedundancy(int8_t* full_redundant_packet,
                                       int full_redundant_packet_size,
index 56ab0cc78722cb558edff64cb58a2013097cda5a..5ee1806668805a09d593991101ffb25219a4f1eb 100644 (file)
@@ -130,7 +130,7 @@ UdpHubListener::~UdpHubListener()
 }
 
 //*******************************************************************************
-// Now that the first handshake is with TCP server, if the addreess/peer port of
+// Now that the first handshake is with TCP server, if the address/peer port of
 // the client is already on the thread pool, it means that a new connection is
 // requested (the old was disconnected). So we have to remove that thread from
 // the pool and then connect again.
@@ -153,7 +153,7 @@ void UdpHubListener::start()
 
     if (mRequireAuth) {
         cout << "JackTrip HUB SERVER: Enabling authentication" << endl;
-        // Check that SSL is avaialable
+        // Check that SSL is available
         bool error = false;
         QString error_message;
         if (!QSslSocket::supportsSsl()) {
@@ -316,7 +316,7 @@ void UdpHubListener::receivedClientInfo(QSslSocket* clientConnection)
 
     // Create a new JackTripWorker, but don't check if this is coming from an existing ip
     // or port yet. We need to wait until we receive the port value from the UDP header to
-    // accomodate NAT.
+    // accommodate NAT.
     // -----------------------------
     int id = getJackTripWorker(PeerAddress.toString(), peer_udp_port, clientName);
 
index 5d361427bd6b3aa0876d8069323cca9992805fd3..4e380327e171dd3c9ba6d0ecbdaaccd1c85bc4bd 100755 (executable)
--- a/src/build
+++ b/src/build
@@ -4,7 +4,7 @@ if [[ -t 1 ]]; then
        echo -e "\033[1;31mIMPORTANT\033[0m"
        echo "The build script is now located in the root jacktrip folder and should"
        echo "be run from there in future. (This script will eventually be removed and"
-       echo "is included for compatability during the transition.)"
+       echo "is included for compatibility during the transition.)"
        echo
        echo "In future, after cloning the repository with git use the following commands:"
        echo -e "\033[1m$ cd jacktrip"
diff --git a/src/dblsqd/feed.cpp b/src/dblsqd/feed.cpp
new file mode 100644 (file)
index 0000000..a24215e
--- /dev/null
@@ -0,0 +1,332 @@
+#include "feed.h"
+
+namespace dblsqd
+{
+
+/*!
+  \class Feed
+ * \brief The Feed class provides methods for accessing DBLSQD Feeds and downloading
+ Releases.
+ *
+ * A Feed is a representation of an Application’s Releases.
+ * This class can retrieve Feeds via HTTP(S) and offers convenience methods for
+ *
+ * \section3 Loading Feeds
+ *
+ * Before a Feed can be loaded with load(), it needs to be initialized with setUrl().
+ *
+ * \section3 Downloading Updates
+ * This class also allows downloading updates through the downloadRelease() method.
+ */
+
+/*!
+ * \brief Constructs a new Feed object.
+ *
+ * \sa setUrl()
+ */
+Feed::Feed(QString baseUrl, QString channel, QString os, QString arch, QString type)
+    : feedReply(NULL)
+    , downloadReply(NULL)
+    , downloadFile(NULL)
+    , redirects(0)
+    , _ready(false)
+{
+    if (!baseUrl.isEmpty()) {
+        this->setUrl(baseUrl, channel, os, arch, type);
+    }
+}
+
+/*!
+ * \brief Sets the Feed URL.
+ *
+ * This method can be used to manually set the Feed URL.
+ */
+void Feed::setUrl(QUrl url)
+{
+    this->url = url;
+}
+
+/*!
+ * \brief Sets the Feed URL by specifying its components.
+ *
+ * The only required component is baseUrl which must be the base URL for an Application
+ * provided by the DBSLQD CLI Tool. It should include the full schema and does not require
+ * a trailing "/".
+ */
+void Feed::setUrl(QString baseUrl, QString channel, QString os, QString arch,
+                  QString type)
+{
+    QStringList urlParts;
+    urlParts << baseUrl;
+    urlParts << channel;
+
+    if (!os.isEmpty()) {
+        urlParts << os;
+    } else {
+        QString autoOs = QSysInfo::productType().toLower();
+        if (autoOs == "windows") {
+            autoOs = "win";
+        } else if (autoOs == "osx" || autoOs == "macos") {
+            autoOs = "mac";
+        } else {
+            autoOs = QSysInfo::kernelType();
+        }
+        urlParts << autoOs;
+    }
+
+    if (!arch.isEmpty()) {
+        urlParts << arch;
+    } else {
+        QString autoArch = QSysInfo::buildCpuArchitecture();
+        if (autoArch == "i386" || autoArch == "i586" || autoArch == "i586") {
+            autoArch = "x86";
+        }
+        urlParts << autoArch;
+    }
+
+    if (!type.isEmpty()) {
+        urlParts << "?t=" + type;
+    }
+
+    this->url = QUrl(urlParts.join("/"));
+}
+
+/*!
+ * \brief Returns the Feed URL.
+ */
+QUrl Feed::getUrl()
+{
+    return QUrl(url);
+}
+
+/*!
+ * \brief Returns a list of all Releases in the Feed.
+ *
+ * The list is sorted in descending order by version number/release date.
+ * If called before ready() was emitted, an empty list is returned.
+ * \sa getReleases()
+ */
+QList<Release> Feed::getReleases()
+{
+    return releases;
+}
+
+/*!
+ * \brief Returns a list of all Releases in the Feed that are newer than the given
+ * Release.
+ *
+ * The list is sorted in descending order by version number/release date.
+ * If called before ready() was emitted, an empty list is returned.
+ * \sa getReleases()
+ */
+QList<Release> Feed::getUpdates(Release currentRelease)
+{
+    QList<Release> updates;
+    for (int i = 0; i < releases.size(); i++) {
+        if (currentRelease < releases.at(i))
+            updates << releases.at(i);
+    }
+    return updates;
+}
+
+/*!
+ * \brief Returns the pointer to a QTemporaryFile for a downloaded file.
+ *
+ * If called before downloadFinished() was emitted, this might return a NULL
+ * pointer.
+ */
+QTemporaryFile* Feed::getDownloadFile()
+{
+    return downloadFile;
+}
+
+/*!
+ * \brief Returns true if Feed information has been retrieved successfully.
+ *
+ * A ready Feed might not contain any release information.
+ * If downloading the Feed failed, false is returned.
+ */
+bool Feed::isReady()
+{
+    return _ready;
+}
+
+/*
+ * Async API functions
+ */
+/*!
+ * \brief Retrieves and parses data from the Feed.
+ *
+ * A Feed URL must have been set before with setUrl(). Emits ready() or loadError() on
+ * completion.
+ */
+void Feed::load()
+{
+    if (feedReply != NULL && !feedReply->isFinished()) {
+        return;
+    }
+
+    QNetworkRequest request(getUrl());
+    feedReply = nam.get(request);
+    connect(feedReply, SIGNAL(finished()), this, SLOT(handleFeedFinished()));
+}
+
+/*!
+ * \brief Starts the download of a given Release.
+ * \sa downloadFinished() downloadError() downloadProgress()
+ */
+void Feed::downloadRelease(Release release)
+{
+    redirects = 0;
+    makeDownloadRequest(release.getDownloadUrl());
+    this->release = release;
+}
+
+/*
+ * Private methods
+ */
+void Feed::makeDownloadRequest(QUrl url)
+{
+    if (downloadReply != NULL && !downloadReply->isFinished()) {
+        disconnect(downloadReply);
+        downloadReply->abort();
+        downloadReply->deleteLater();
+    }
+    if (downloadFile != NULL) {
+        disconnect(downloadFile);
+        downloadFile->close();
+        downloadFile->deleteLater();
+        downloadFile = NULL;
+    }
+
+    QNetworkRequest request(url);
+    downloadReply = nam.get(request);
+    connect(downloadReply, SIGNAL(downloadProgress(qint64, qint64)), this,
+            SLOT(handleDownloadProgress(qint64, qint64)));
+    connect(downloadReply, SIGNAL(readyRead()), this, SLOT(handleDownloadReadyRead()));
+    connect(downloadReply, SIGNAL(finished()), this, SLOT(handleDownloadFinished()));
+}
+
+/*
+ * Signals
+ */
+/*! \fn void Feed::ready()
+ * This signal is emitted when a Feed has been successfully downloaded and parsed.
+ * \sa loadError() load()
+ */
+
+/*! \fn void Feed::loadError(QString message)
+ * This signal is emitted when a Feed could not be downloaded.
+ * When loadError() is emitted, ready() is not emitted.
+ * \sa ready() load()
+ */
+
+/*! \fn void Feed::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+ * This signal is emitted during the download of a Release through downloadRelease().
+ * \sa downloadRelease()
+ */
+
+/*! \fn void Feed::downloadFinished()
+ * This signal is emitted when the download of a Release was successful.
+ * A QTemporaryFile* of the downloaded file can then be retrieved with getDownloadFile().
+ * \sa downloadRelease()
+ */
+
+/*! \fn void Feed::downloadError()
+ * This signal is emitted when there was an error downloading or verifying a Release.
+ * When downloadError() is emitted, downloadFinished() is not emitted.
+ * \sa downloadFinished() downloadRelease()
+ */
+
+/*
+ * Private Slots
+ */
+void Feed::handleFeedFinished()
+{
+    if (feedReply->error() != QNetworkReply::NoError) {
+        emit loadError(feedReply->errorString());
+        return;
+    }
+
+    releases.clear();
+    QByteArray json         = feedReply->readAll();
+    QJsonDocument doc       = QJsonDocument::fromJson(json);
+    QJsonArray releasesInfo = doc.object().value("releases").toArray();
+    for (int i = 0; i < releasesInfo.size(); i++) {
+        releases << Release(releasesInfo.at(i).toObject());
+    }
+    std::sort(releases.begin(), releases.end());
+    std::reverse(releases.begin(), releases.end());
+
+    _ready = true;
+    emit ready();
+}
+
+void Feed::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+    emit downloadProgress(bytesReceived, bytesTotal);
+}
+
+void Feed::handleDownloadReadyRead()
+{
+    if (downloadFile == NULL) {
+        QString fileName = downloadReply->url().fileName();
+        // Workaround for dblsqd to extract filename via query params from a
+        // Github-formatted redirect URL
+        QUrl url     = downloadReply->url();
+        QString host = url.host();
+        if (host.contains("github", Qt::CaseInsensitive) && url.hasQuery()) {
+            QString query = url.query();
+            QRegExp rx("filename%3D(.*)(&|$)");
+            rx.setMinimal(true);
+            if (rx.indexIn(query) > -1) {
+                fileName = rx.cap(1);
+            }
+        }
+        // End workaround
+        int extensionPos = fileName.indexOf(QRegExp("(?:\\.tar)?\\.[a-zA-Z0-9]+$"));
+        if (extensionPos > -1) {
+            fileName.insert(extensionPos, "-XXXXXX");
+        }
+        downloadFile = new QTemporaryFile(QDir::tempPath() + "/" + fileName);
+        downloadFile->open();
+    }
+    downloadFile->write(downloadReply->readAll());
+}
+
+void Feed::handleDownloadFinished()
+{
+    if (downloadReply->error() != QNetworkReply::NoError) {
+        emit downloadError(downloadReply->errorString());
+        return;
+    } else if (!downloadReply->attribute(QNetworkRequest::RedirectionTargetAttribute)
+                    .isNull()) {
+        if (redirects >= 8) {
+            emit downloadError(tr("Too many redirects."));
+            return;
+        }
+        QUrl redirectionTarget =
+            downloadReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
+        QUrl redirectedUrl = downloadReply->url().resolved(redirectionTarget);
+        redirects++;
+        makeDownloadRequest(redirectedUrl);
+        return;
+    } else if (downloadFile == NULL) {
+        emit downloadError(tr("No data received from server"));
+        return;
+    }
+
+    downloadFile->flush();
+    downloadFile->seek(0);
+    QCryptographicHash fileHash(QCryptographicHash::Sha256);
+    fileHash.addData(downloadFile->readAll());
+    QString hashResult = fileHash.result().toHex();
+    if (hashResult.toLower() != release.getDownloadSHA256().toLower()) {
+        emit downloadError(tr("Could not verify download integrity."));
+        return;
+    }
+
+    emit downloadFinished();
+}
+
+}  // namespace dblsqd
diff --git a/src/dblsqd/feed.h b/src/dblsqd/feed.h
new file mode 100644 (file)
index 0000000..fc030ac
--- /dev/null
@@ -0,0 +1,67 @@
+#ifndef DBLSQD_FEED_H
+#define DBLSQD_FEED_H
+
+#include <QtCore>
+#include <QtNetwork>
+
+#include "release.h"
+
+namespace dblsqd
+{
+
+class Feed : public QObject
+{
+    Q_OBJECT
+
+   public:
+    Feed(QString baseUrl = "", QString channel = "release", QString os = QString(),
+         QString arch = QString(), QString type = QString());
+
+    void setUrl(QUrl url);
+    void setUrl(QString baseUrl, QString channel = "release", QString os = QString(),
+                QString arch = QString(), QString type = QString());
+    QUrl getUrl();
+
+    // Async API
+    void load();
+    void downloadRelease(Release release);
+
+    // Sync API
+    QList<Release> getUpdates(
+        Release currentRelease = Release(QCoreApplication::applicationVersion()));
+    QList<Release> getReleases();
+    QTemporaryFile* getDownloadFile();
+    bool isReady();
+
+   signals:
+    void ready();
+    void loadError(QString message);
+    void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+    void downloadFinished();
+    void downloadError(QString message);
+
+   private:
+    QUrl url;
+
+    QList<Release> releases;
+
+    void makeDownloadRequest(QUrl url);
+
+    QNetworkAccessManager nam;
+    QNetworkReply* feedReply;
+    Release release;
+    QNetworkReply* downloadReply;
+    QTemporaryFile* downloadFile;
+    uint redirects;
+    bool _ready;
+
+   private slots:
+    void handleFeedFinished();
+    void handleDownloadProgress(qint64, qint64);
+    void handleDownloadReadyRead();
+    void handleDownloadFinished();
+};
+
+}  // namespace dblsqd
+
+#endif  // DBLSQD_FEED_H
diff --git a/src/dblsqd/release.cpp b/src/dblsqd/release.cpp
new file mode 100644 (file)
index 0000000..e1c728a
--- /dev/null
@@ -0,0 +1,143 @@
+#include "release.h"
+
+namespace dblsqd
+{
+
+/*!
+ * \class Release
+ * \brief This class is used to represent information about a single Release
+ * from a Feed.
+ */
+
+/*!
+ * \brief Constructs a new Release from q QJsonObject.
+ */
+Release::Release(QJsonObject releaseInfo)
+{
+    this->version   = releaseInfo.value("version").toString();
+    this->changelog = releaseInfo.value("changelog").toString();
+
+    QJsonObject downloadInfo = releaseInfo.value("download").toObject();
+    this->date =
+        QDateTime::fromString(downloadInfo.value("date").toString(), Qt::ISODate);
+    this->downloadUrl    = QUrl(downloadInfo.value("url").toString());
+    this->downloadSize   = downloadInfo.value("size").toDouble();
+    this->downloadSHA1   = downloadInfo.value("sha1").toString();
+    this->downloadSHA256 = downloadInfo.value("sha256").toString();
+    this->downloadDSA    = downloadInfo.value("dsa").toString();
+}
+
+/*!
+ * \brief Constructs a new Release from a version string and a date.
+ *
+ * This method is useful when constructing a "virtual" Release for comparing
+ * it with Releases retrieved from a Feed.
+ */
+Release::Release(QString version, QDateTime date)
+    : version(version)
+    , date(date)
+    , changelog("")
+    , downloadUrl("")
+    , downloadSize(0)
+    , downloadSHA1("")
+    , downloadSHA256("")
+    , downloadDSA("")
+{
+}
+
+/*!
+ * \brief Compares two Releases.
+ *
+ * If the Release version is compatible with SemVer, the version determines
+ * Release order. If versions are identical or not compatible with SemVer,
+ * Release date is used for determining order instead.
+ */
+bool operator<(const Release& one, const Release& other)
+{
+    SemVer v1(one.version);
+    SemVer v2(other.version);
+    if (v1.isValid() && v2.isValid()) {
+        return (v1 < v2);
+    } else {
+        return (one.date < other.date);
+    }
+}
+
+bool operator==(const Release& one, const Release& other)
+{
+    return one.version == other.version;
+}
+
+bool operator<=(const Release& one, const Release& other)
+{
+    return one == other || one < other;
+}
+
+/*
+ * Getters
+ */
+/*!
+ * \brief Returns the Release version.
+ */
+QString Release::getVersion() const
+{
+    return this->version;
+}
+
+/*!
+ * \brief Returns the Release changelog.
+ */
+QString Release::getChangelog() const
+{
+    return this->changelog;
+}
+
+/*!
+ * \brief Returns the Release date.
+ */
+QDateTime Release::getDate() const
+{
+    return this->date;
+}
+
+/*!
+ * \brief Returns the Release download URL.
+ */
+QUrl Release::getDownloadUrl() const
+{
+    return this->downloadUrl;
+}
+
+/*!
+ * \brief Returns the SHA1 hash of the Release download.
+ */
+QString Release::getDownloadSHA1() const
+{
+    return this->downloadSHA1;
+}
+
+/*!
+ * \brief Returns the SHA256 hash of the Release download.
+ */
+QString Release::getDownloadSHA256() const
+{
+    return this->downloadSHA256;
+}
+
+/*!
+ * \brief Returns the DSA signature of the Release download.
+ */
+QString Release::getDownloadDSA() const
+{
+    return this->downloadDSA;
+}
+
+/*!
+ * \brief Returns the size of the Release download in bytes.
+ */
+qint64 Release::getDownloadSize() const
+{
+    return (qint64)this->downloadSize;
+}
+
+}  // namespace dblsqd
diff --git a/src/dblsqd/release.h b/src/dblsqd/release.h
new file mode 100644 (file)
index 0000000..9c769bf
--- /dev/null
@@ -0,0 +1,44 @@
+#ifndef DBLSQD_RELEASE_H
+#define DBLSQD_RELEASE_H
+
+#include <QUrl>
+#include <QtCore>
+
+#include "semver.h"
+
+namespace dblsqd
+{
+
+class Release
+{
+   public:
+    Release(QJsonObject releaseInfo);
+    Release(QString version = QString(), QDateTime date = QDateTime());
+
+    friend bool operator<(const Release& one, const Release& other);
+    friend bool operator==(const Release& one, const Release& other);
+    friend bool operator<=(const Release& one, const Release& other);
+
+    QString getVersion() const;
+    QString getChangelog() const;
+    QDateTime getDate() const;
+    QUrl getDownloadUrl() const;
+    QString getDownloadSHA1() const;
+    QString getDownloadSHA256() const;
+    QString getDownloadDSA() const;
+    qint64 getDownloadSize() const;
+
+   private:
+    QString version;
+    QDateTime date;
+    QString changelog;
+    QUrl downloadUrl;
+    long downloadSize;
+    QString downloadSHA1;
+    QString downloadSHA256;
+    QString downloadDSA;
+};
+
+}  // namespace dblsqd
+
+#endif  // DBLSQD_RELEASE_H
diff --git a/src/dblsqd/semver.cpp b/src/dblsqd/semver.cpp
new file mode 100644 (file)
index 0000000..0aa77d0
--- /dev/null
@@ -0,0 +1,87 @@
+#include "semver.h"
+
+namespace dblsqd
+{
+
+/*!
+ * \class SemVer
+ * \brief SemVer encapsulates a version according to
+ * Semantic Versioning 2.0.
+ */
+
+/*!
+ * \brief Constructs a new SemVer object from a string.
+ */
+SemVer::SemVer(QString version) : original(version), valid(false)
+{
+    QRegExp rx(getRegExp());
+    if (rx.indexIn(version) > -1) {
+        this->major      = rx.cap(1).toInt();
+        this->minor      = rx.cap(2).toInt();
+        this->patch      = rx.cap(3).toInt();
+        this->prerelease = rx.cap(4);
+        this->build      = rx.cap(5);
+        this->valid      = true;
+    } else {
+        this->major      = 0;
+        this->minor      = 0;
+        this->patch      = 0;
+        this->prerelease = "";
+        this->build      = "";
+        this->valid      = false;
+    }
+}
+
+/*!
+ * \brief Returns true if this version is valid according to the SemVer
+ * specification. Otherwise returns false.
+ */
+bool SemVer::isValid() const
+{
+    return this->valid;
+}
+
+/*!
+ * \brief Compares two SemVer objects.
+ *
+ * Returns true if the left-hand SemVer object represents a lower version
+ * according to the SemVer 2.0 specification.
+ * Otherweise returns false.
+ * Returns false if one of the SemVer objects does not represent a valid
+ * SemVer.
+ * \sa isValid()
+ */
+bool SemVer::operator<(const SemVer& other)
+{
+    if (!this->isValid() || !other.isValid()) {
+        return false;
+    }
+
+    if (this->major != other.major) {
+        return this->major < other.major;
+    } else if (this->minor != other.minor) {
+        return this->minor < other.minor;
+    } else if (this->patch != other.patch) {
+        return this->patch < other.patch;
+    } else if (this->prerelease != other.prerelease) {
+        if (this->prerelease == "") {
+            return false;
+        } else if (other.prerelease == "") {
+            return true;
+        }
+        return (QString::localeAwareCompare(this->prerelease, other.prerelease) < 0);
+    } else {
+        return (QString::localeAwareCompare(this->build, other.build) < 0);
+    }
+}
+
+QString SemVer::getRegExp()
+{
+    QString v = "(0|[1-9]\\d*)";
+    QString p =
+        "(?:-((?:0|[1-9A-Za-z][0-9A-Za-z]*)(?:\\.(?:0|[1-9A-Za-z][0-9A-Za-z]*))*))?";
+    QString b = "(?:\\+((?:[0-9A-Za-z]*)(?:\\.(?:[0-9A-Za-z][0-9A-Za-z]*))*))?";
+    return "^" + v + "." + v + "." + v + p + b + "$";
+}
+
+}  // namespace dblsqd
diff --git a/src/dblsqd/semver.h b/src/dblsqd/semver.h
new file mode 100644 (file)
index 0000000..18ebcda
--- /dev/null
@@ -0,0 +1,34 @@
+#ifndef DBLSQD_SEMVER_H
+#define DBLSQD_SEMVER_H
+
+#include <QObject>
+#include <QRegExp>
+
+namespace dblsqd
+{
+
+class SemVer
+{
+   public:
+    SemVer(QString version);
+
+    bool operator<(const SemVer& other);
+
+    bool isValid() const;
+    QString toString();
+
+   private:
+    QString original;
+    int major;
+    int minor;
+    int patch;
+    QString prerelease;
+    QString build;
+    bool valid;
+
+    static QString getRegExp();
+};
+
+}  // namespace dblsqd
+
+#endif  // DBLSQD_SEMVER_H
diff --git a/src/dblsqd/update_dialog.cpp b/src/dblsqd/update_dialog.cpp
new file mode 100644 (file)
index 0000000..bb914ef
--- /dev/null
@@ -0,0 +1,694 @@
+#include "update_dialog.h"
+
+#include "ui_update_dialog.h"
+
+namespace dblsqd
+{
+
+/*!
+ * \class UpdateDialog
+ * \brief A dialog class for displaying and downloading update information.
+ *
+ * UpdateDialog is a drop-in class for adding a fully-functional auto-update
+ * component to an existing application.
+ *
+ * The most simple integration is
+ * possible with just three lines of code:
+ * \code
+ * dblsqd::Feed* feed = new dblsqd::Feed();
+ * feed->setUrl("https://feeds.dblsqd.com/:app_token");
+ * dblsqd::UpdateDialog* updateDialog = new dblsqd::UpdateDialog(feed);
+ * \endcode
+ *
+ * The update dialog can also display an application icon which can be set with
+ * setIcon().
+ */
+
+/*!
+ * \enum UpdateDialog::Type
+ * \brief This flag determines the if and when the UpdateDialog is displayed
+ * automatically.
+ *
+ * *OnUpdateAvailable*: Automatically display the dialog as soon as the Feed
+ * has been downloaded and parsed and if there is a newer version than the
+ * current version returned by QCoreApplication::applicationVersion().
+ *
+ * *OnLastWindowClosed*: If there is a newer version available than the current
+ * version returned by QCoreApplication::applicationVersion(), the update
+ * dialog is displayed when QGuiApplication emits the lastWindowClosed() event.
+ * Note that when this flag is used,
+ * QGuiApplication::setQuitOnLastWindowClosed(false) will be called.
+ *
+ * *Manual*: The dialog is only displayed when explicitly requested via show()
+ * or exec().
+ * Note that update information might not be available instantly after
+ * constructing an UpdateDialog.
+ *
+ * *ManualChangelog*: The dialog is only displayed when explicitly requested via
+ * show() or exec().
+ * Instead of the full update interface, only the changelog will be shown.
+ */
+
+/*!
+ * \brief Constructs a new UpdateDialog.
+ *
+ * A Feed object needs to be constructed first and passed to this constructor.
+ * Feed::load() does not need to be called on the Feed object.
+ *
+ * The given UpdateDialog::Type flag determines when/if the dialog is shown
+ * automatically.
+ *
+ * UpdateDialog uses QSettings to save information such as when a release was
+ * skipped by the users. If you want to use a specially initialized QSettings
+ * object, you may also pass it to this constructor.
+ *
+ */
+UpdateDialog::UpdateDialog(Feed* feed, int type, QWidget* parent, QSettings* settings)
+    : QDialog(parent)
+    , ui(new Ui::UpdateDialog)
+    , feed(feed)
+    , type(type)
+    , settings(settings)
+    , accepted(false)
+    , isDownloadFinished(false)
+    , acceptedInstallButton(NULL)
+{
+    ui->setupUi(this);
+
+    QPalette palette        = this->palette();
+    QString textColor       = palette.color(QPalette::Text).name();
+    QString backgroundColor = palette.color(QPalette::Base).name();
+    QString labelChangelogStyle =
+        QString("color: %1; background: %2").arg(textColor, backgroundColor);
+    ui->labelChangelog->setStyleSheet(labelChangelogStyle);
+
+    ui->buttonCancel->addAction(ui->actionCancel);
+    ui->buttonCancel->addAction(ui->actionSkip);
+    ui->buttonCancel->setDefaultAction(ui->actionCancel);
+
+    _openExternalLinks = true;
+    connect(ui->labelChangelog, SIGNAL(linkActivated(QString)), this,
+            SLOT(onLinkActivated(QString)));
+
+    switch (type) {
+    case OnUpdateAvailable: {
+        connect(this, SIGNAL(ready()), this, SLOT(showIfUpdatesAvailable()));
+        break;
+    }
+    case OnLastWindowClosed: {
+        QGuiApplication* app = (QGuiApplication*)QApplication::instance();
+        app->setQuitOnLastWindowClosed(false);
+        connect(app, SIGNAL(lastWindowClosed()), this,
+                SLOT(showIfUpdatesAvailableOrQuit()));
+        break;
+    }
+    case Manual: {
+        // don’t do anything
+    }
+    }
+
+    if (feed->isReady()) {
+        handleFeedReady();
+    } else {
+        setupLoadingUi();
+        feed->load();
+        connect(feed, SIGNAL(ready()), this, SLOT(handleFeedReady()));
+    }
+}
+
+UpdateDialog::~UpdateDialog()
+{
+    delete ui;
+}
+
+/*
+ * Setters
+ */
+/*!
+ * \brief Sets the icon displayed in the update window.
+ */
+void UpdateDialog::setIcon(QPixmap pixmap)
+{
+    ui->labelIcon->setPixmap(QPixmap(pixmap));
+    ui->labelIcon->setHidden(false);
+}
+
+void UpdateDialog::setIcon(QString fileName)
+{
+    ui->labelIcon->setPixmap(QPixmap(fileName));
+    ui->labelIcon->setHidden(false);
+}
+
+/*!
+ * \brief Sets the minimum version to be displayed in the changelog.
+ * Defaults to QApplication::applicationVersion() if not set.
+ * \param version
+ */
+void UpdateDialog::setMinVersion(QString version)
+{
+    _minVersion = version;
+    setupChangelogUi();
+}
+
+/*!
+ * \brief Sets the maximum version to be displayed in the changelog
+ * \param version
+ */
+void UpdateDialog::setMaxVersion(QString version)
+{
+    _maxVersion = version;
+    setupChangelogUi();
+}
+
+/*!
+ * \brief Convenience method for setting minimum and maximum version to be displayed in
+ * the changelog. maximumVersion is set to QApplication::applicationVersion()
+ *
+ * \param previousVersion
+ */
+void UpdateDialog::setPreviousVersion(QString previousVersion)
+{
+    _previousVersion = previousVersion;
+    _minVersion      = previousVersion;
+    _maxVersion      = QApplication::applicationVersion();
+    setupChangelogUi();
+}
+
+/*!
+ * \brief Adds a custom button for handling update installation.
+ * \param button
+ *
+ * When the custom button is clicked after an update has been downloaded or when
+ * downloading an update that was started by clicking the button has finished,
+ * installButtonClicked(QAbstractButton* button, QString filePath) is emitted.
+ */
+void UpdateDialog::addInstallButton(QAbstractButton* button)
+{
+    installButtons.append(button);
+    ui->buttonContainer->layout()->addWidget(button);
+    if (isVisible() && ui->buttonCancel->isVisible()) {
+        setupUpdateUi();
+    }
+}
+
+/*!
+ * \propget UpdateExternalLinks
+ *
+ * Determines if links in the changelog should be opened automatically by
+ * QDesktopServices::openUrl() when a user clicks on them.
+ * If set to false, the linkActivated() signal is emitted instead.
+ *
+ * The default value is true.
+
+ */
+bool UpdateDialog::openExternalLinks()
+{
+    return _openExternalLinks;
+}
+
+/*!
+ * \propset UpdateDialog::openExternalLinks
+ */
+void UpdateDialog::setOpenExternalLinks(bool open)
+{
+    _openExternalLinks = open;
+}
+
+/*
+ * Public Slots
+ */
+/*!
+ * \brief Default handler for the install button.
+ *
+ * Closes the dialog if no other action (such as
+ * downloading or installing a Release) is required first.
+ */
+void UpdateDialog::onButtonInstall()
+{
+    accepted = true;
+    if (isDownloadFinished) {
+        startUpdate();
+    } else if (!latestRelease.getVersion().isEmpty()) {
+        startDownload();
+    } else {
+        done(QDialog::Accepted);
+    }
+}
+
+void UpdateDialog::onButtonCustomInstall()
+{
+    accepted = true;
+    if (isDownloadFinished) {
+        emit installButtonClicked((QAbstractButton*)sender(), updateFilePath);
+    } else if (!latestRelease.getVersion().isEmpty()) {
+        acceptedInstallButton = (QAbstractButton*)sender();
+        startDownload();
+    } else {
+        done(QDialog::Accepted);
+    }
+}
+
+/*!
+ * \brief Skips the latest retrieved Release.
+ *
+ * If a release has been skipped, UpdateDialog will not be displayed
+ * automatically when using Type::OnUpdateAvailable or
+ * Type::OnLastWindowClosed.
+ */
+void UpdateDialog::skip()
+{
+    if (!updateFilePath.isEmpty()) {
+        QFile::remove(updateFilePath);
+    }
+    setSettingsValue("skipRelease", latestRelease.getVersion(), settings);
+    done(QDialog::Rejected);
+}
+
+/*!
+ * \brief Shows the dialog if there are available updates.
+ */
+void UpdateDialog::showIfUpdatesAvailable()
+{
+    QString latestVersion = latestRelease.getVersion();
+    bool skipRelease =
+        (settingsValue("skipRelease", "", settings).toString() == latestVersion);
+    if (!latestVersion.isEmpty() && !skipRelease) {
+        show();
+    }
+}
+
+/*!
+ * \brief Shows the dialog if there are updates available or quits the application.
+ */
+void UpdateDialog::showIfUpdatesAvailableOrQuit()
+{
+    if (type == OnLastWindowClosed) {
+        QGuiApplication* app = (QGuiApplication*)QApplication::instance();
+        app->setQuitOnLastWindowClosed(true);
+        disconnect(app, SIGNAL(lastWindowClosed()), this,
+                   SLOT(showIfUpdatesAvailableOrQuit()));
+    }
+    QString latestVersion = latestRelease.getVersion();
+    bool skipRelease =
+        (settingsValue("skipRelease", "", settings).toString() == latestVersion);
+    if (!latestVersion.isEmpty() && !skipRelease) {
+        show();
+    } else {
+        QCoreApplication::quit();
+    }
+}
+
+/*
+ * Static settings helpers
+ */
+QVariant UpdateDialog::settingsValue(QString key, QVariant defaultValue,
+                                     QSettings* settings)
+{
+    return settings->value("DBLSQD/" + key, defaultValue);
+}
+
+void UpdateDialog::setSettingsValue(QString key, QVariant value, QSettings* settings)
+{
+    settings->setValue("DBLSQD/" + key, value);
+}
+
+void UpdateDialog::removeSetting(QString key, QSettings* settings)
+{
+    settings->remove("DBLSQD/" + key);
+}
+
+void UpdateDialog::setDefaultSettingsValue(QString key, QVariant value,
+                                           QSettings* settings)
+{
+    if (settings->contains("DBLSQD/" + key))
+        return;
+    setSettingsValue(key, value, settings);
+}
+
+/*!
+ * \brief Enables or disables automatic downloads.
+ */
+void UpdateDialog::enableAutoDownload(bool enabled, QSettings* settings)
+{
+    setSettingsValue("autoDownload", enabled, settings);
+}
+
+/*!
+ * \brief Returns true if automatic downloads are enabled.
+ *
+ * If defaultValue is provided, it is stored if no other value has previously been set.
+ */
+bool UpdateDialog::autoDownloadEnabled(QVariant defaultValue, QSettings* settings)
+{
+    if (defaultValue.isValid()) {
+        setDefaultSettingsValue("autoDownload", defaultValue, settings);
+    } else {
+        defaultValue = false;
+    }
+    return settingsValue("autoDownload", defaultValue, settings).toBool();
+}
+
+/*!
+ * \overload
+ */
+bool UpdateDialog::autoDownloadEnabled(QSettings* settings)
+{
+    return settingsValue("autoDownload", false, settings).toBool();
+}
+
+/*
+ * Helpers
+ */
+
+void UpdateDialog::adjustDialogSize()
+{
+    adjustSize();
+
+/*HACK: Qt seems to incorrectly calculate window geometry on Windows.
+        This code avoids warning messages logged by the application
+        in that case.*/
+#if defined(Q_OS_WIN) || defined(Q_WS_WIN)
+    QSize dialogSize = size();
+    resize(dialogSize.width(), dialogSize.height() + 3);
+#endif
+}
+
+void UpdateDialog::resetUi()
+{
+    QList<QWidget*> hiddenWidgets;
+    for (int i = 0; i < installButtons.size(); i++) {
+        hiddenWidgets << installButtons.at(i);
+    }
+    hiddenWidgets << ui->headerContainer << ui->labelIcon << ui->headerContainerLoading
+                  << ui->headerContainerNoUpdates << ui->headerContainerChangelog
+                  << ui->scrollAreaChangelog << ui->progressBar << ui->checkAutoDownload
+                  << ui->buttonCancel << ui->buttonCancelLoading << ui->buttonConfirm
+                  << ui->buttonInstall;
+    for (int i = 0; i < hiddenWidgets.size(); i++) {
+        hiddenWidgets.at(i)->hide();
+        hiddenWidgets.at(i)->disconnect();
+    }
+    ui->progressBar->reset();
+    adjustDialogSize();
+}
+
+void UpdateDialog::setupLoadingUi()
+{
+    resetUi();
+    ui->headerContainerLoading->show();
+    ui->progressBar->show();
+    ui->progressBar->setMaximum(0);
+    ui->progressBar->setMinimum(0);
+    ui->buttonCancelLoading->show();
+    ui->buttonCancelLoading->setFocus();
+    connect(ui->buttonCancelLoading, SIGNAL(clicked(bool)), this, SLOT(reject()));
+    adjustDialogSize();
+}
+
+void UpdateDialog::setupUpdateUi()
+{
+    resetUi();
+
+    QList<QWidget*> showWidgets;
+    showWidgets << ui->headerContainer << ui->scrollAreaChangelog << ui->checkAutoDownload
+                << ui->buttonCancel << ui->buttonInstall;
+    for (int i = 0; i < showWidgets.size(); i++) {
+        showWidgets.at(i)->show();
+    }
+
+    QList<QLabel*> labels;
+    labels << ui->labelHeadline << ui->labelInfo;
+    for (int i = 0; i < labels.size(); i++) {
+        QString text = labels.at(i)->text();
+        replaceAppVars(text);
+        labels.at(i)->setText(text);
+    }
+    ui->labelChangelog->setText(generateChangelogDocument());
+
+    ui->checkAutoDownload->setChecked(autoDownloadEnabled(settings));
+
+    // Adapt buttons if release has been downloaded already
+    if (isDownloadFinished) {
+        ui->progressBar->show();
+        ui->progressBar->setMaximum(1);
+        ui->progressBar->setValue(1);
+    }
+
+    connect(feed, SIGNAL(downloadFinished()), this, SLOT(handleDownloadFinished()));
+    connect(feed, SIGNAL(downloadError(QString)), this,
+            SLOT(handleDownloadError(QString)));
+    connect(feed, SIGNAL(downloadProgress(qint64, qint64)), this,
+            SLOT(updateProgressBar(qint64, qint64)));
+
+    connect(ui->buttonConfirm, SIGNAL(clicked()), this, SLOT(accept()));
+    connect(ui->actionCancel, SIGNAL(triggered()), this, SLOT(reject()));
+    connect(ui->actionSkip, SIGNAL(triggered()), this, SLOT(skip()));
+    connect(ui->checkAutoDownload, SIGNAL(toggled(bool)), this,
+            SLOT(autoDownloadCheckboxToggled(bool)));
+
+    // Install buttons
+    if (installButtons.isEmpty()) {
+        ui->buttonInstall->setFocus();
+        connect(ui->buttonInstall, SIGNAL(clicked()), this, SLOT(onButtonInstall()));
+    } else {
+        ui->buttonInstall->hide();
+        for (int i = 0; i < installButtons.size(); i++) {
+            installButtons.at(i)->show();
+            connect(installButtons.at(i), SIGNAL(clicked(bool)), this,
+                    SLOT(onButtonCustomInstall()));
+        }
+        installButtons.last()->setFocus();
+    }
+
+    adjustDialogSize();
+}
+
+void UpdateDialog::setupChangelogUi()
+{
+    resetUi();
+
+    QList<QWidget*> showWidgets;
+    showWidgets << ui->headerContainerChangelog << ui->buttonConfirm
+                << ui->scrollAreaChangelog;
+    for (int i = 0; i < showWidgets.size(); i++) {
+        showWidgets.at(i)->show();
+    }
+    QList<QLabel*> labels;
+    labels << ui->labelHeadlineChangelog << ui->labelInfoChangelog;
+    for (int i = 0; i < labels.size(); i++) {
+        QString text = labels.at(i)->text();
+        replaceAppVars(text);
+        labels.at(i)->setText(text);
+    }
+    ui->labelChangelog->setText(generateChangelogDocument());
+    connect(ui->buttonConfirm, SIGNAL(clicked(bool)), this, SLOT(accept()));
+    ui->buttonConfirm->setFocus();
+    adjustDialogSize();
+}
+
+void UpdateDialog::setupNoUpdatesUi()
+{
+    resetUi();
+    QList<QWidget*> showWidgets;
+    showWidgets << ui->headerContainerNoUpdates << ui->buttonConfirm;
+    for (int i = 0; i < showWidgets.size(); i++) {
+        showWidgets.at(i)->show();
+    }
+    ui->buttonConfirm->setFocus();
+
+    QString text = ui->labelHeadlineNoUpdates->text();
+    replaceAppVars(text);
+    ui->labelHeadlineNoUpdates->setText(text);
+
+    connect(ui->buttonConfirm, SIGNAL(clicked(bool)), this, SLOT(accept()));
+    adjustDialogSize();
+}
+
+void UpdateDialog::disableButtons(bool disable)
+{
+    QList<QWidget*> buttons;
+    for (int i = 0; i < installButtons.size(); i++) {
+        buttons << installButtons.at(i);
+    }
+    buttons << ui->buttonCancel << ui->buttonCancelLoading << ui->buttonConfirm
+            << ui->buttonConfirm << ui->buttonInstall << ui->checkAutoDownload;
+    for (int i = 0; i < buttons.size(); i++) {
+        buttons.at(i)->setDisabled(disable);
+    }
+}
+
+void UpdateDialog::replaceAppVars(QString& string)
+{
+    string.replace("%APPNAME%", QCoreApplication::applicationName());
+    string.replace("%CURRENT_VERSION%", QCoreApplication::applicationVersion());
+    string.replace("%UPDATE_VERSION%", latestRelease.getVersion());
+}
+
+QString UpdateDialog::generateChangelogDocument()
+{
+    QString changelog;
+    QList<Release> changelogReleases;
+    if (_minVersion.isEmpty() && _maxVersion.isEmpty()) {
+        changelogReleases = updates;
+    } else {
+        Release minRelease(_minVersion.isEmpty() ? QApplication::applicationVersion()
+                                                 : _minVersion);
+        Release maxRelease(_maxVersion);
+        for (int i = 0; i < releases.size(); i++) {
+            if (minRelease < releases.at(i)
+                && (_maxVersion.isEmpty() || releases.at(i) <= maxRelease)) {
+                changelogReleases << releases.at(i);
+            }
+        }
+    }
+    for (int i = 0; i < changelogReleases.size(); i++) {
+        QString h2Style = "font-size: medium;";
+        if (i > 0) {
+            h2Style.append("margin-top: 1em;");
+        }
+        changelog.append("<h2 style=\"" + h2Style + "\">"
+                         + changelogReleases.at(i).getVersion() + "</h2>");
+        changelog.append("<p>" + changelogReleases.at(i).getChangelog() + "</p>");
+    }
+    return changelog;
+}
+
+void UpdateDialog::startDownload()
+{
+    feed->downloadRelease(latestRelease);
+    disableButtons(true);
+}
+
+void UpdateDialog::startUpdate()
+{
+    if (QDesktopServices::openUrl(QUrl::fromLocalFile(updateFilePath))) {
+        done(QDialog::Accepted);
+        QApplication::quit();
+    } else {
+        handleDownloadError(tr("Could not open downloaded file %1").arg(updateFilePath));
+    }
+}
+
+/*
+ * Private Slots
+ */
+
+void UpdateDialog::autoDownloadCheckboxToggled(bool enabled)
+{
+    enableAutoDownload(enabled, settings);
+}
+
+void UpdateDialog::handleFeedReady()
+{
+    // Retrieve update information
+    Release currentRelease(QApplication::applicationVersion());
+    updates  = feed->getUpdates(currentRelease);
+    releases = feed->getReleases();
+    if (!updates.isEmpty()) {
+        latestRelease = updates.first();
+    }
+
+    if (type == ManualChangelog) {
+        setupChangelogUi();
+        emit ready();
+        return;
+    }
+
+    // Check if an update has been downloaded previously
+    updateFilePath = settingsValue("updateFilePath", "", settings).toString();
+    if (!updateFilePath.isEmpty() && QFile::exists(updateFilePath)) {
+        QString updateFileVersion =
+            settingsValue("updateFileVersion", "", settings).toString();
+        if (updateFileVersion != latestRelease.getVersion()
+            || updateFileVersion == QApplication::applicationVersion()) {
+            QFile::remove(updateFilePath);
+            removeSetting("updateFilePath");
+            removeSetting("updateFileVersion");
+            updateFilePath = "";
+        } else {
+            isDownloadFinished = true;
+        }
+    }
+
+    // Check if there are any updates
+    if (updates.isEmpty()) {
+        setupNoUpdatesUi();
+        return;
+    }
+
+    // Automatic downloads
+    QString latestVersion = latestRelease.getVersion();
+    bool skipRelease =
+        (settingsValue("skipRelease", "", settings).toString() == latestVersion);
+    bool autoDownload = autoDownloadEnabled(settings) && (!skipRelease);
+    if (autoDownload && !isDownloadFinished) {
+        startDownload();
+    }
+
+    // Setup UI
+    setupUpdateUi();
+    emit ready();
+}
+
+void UpdateDialog::handleDownloadFinished()
+{
+    QTemporaryFile* file = feed->getDownloadFile();
+    isDownloadFinished   = true;
+    updateFilePath       = file->fileName();
+    file->setAutoRemove(false);
+    file->close();
+    file->deleteLater();
+    setSettingsValue("updateFilePath", updateFilePath, settings);
+    setSettingsValue("updateFileVersion", latestRelease.getVersion(), settings);
+
+    if (accepted) {
+        if (acceptedInstallButton == NULL) {
+            startUpdate();
+        } else {
+            emit installButtonClicked(acceptedInstallButton, updateFilePath);
+        }
+
+    } else {
+        disableButtons(false);
+    }
+}
+
+void UpdateDialog::handleDownloadError(QString message)
+{
+    QMessageBox* messageBox = new QMessageBox(this);
+    messageBox->setIcon(QMessageBox::Warning);
+    messageBox->setText("There was an error while downloading the update.");
+    messageBox->setInformativeText(message);
+    messageBox->show();
+    done(QDialog::Rejected);
+}
+
+void UpdateDialog::updateProgressBar(qint64 bytesReceived, qint64 bytesTotal)
+{
+    ui->progressBar->show();
+    ui->progressBar->setMaximum(bytesTotal / 1024);
+    ui->progressBar->setValue(bytesReceived / 1024);
+}
+
+void UpdateDialog::onLinkActivated(QString link)
+{
+    if (_openExternalLinks) {
+        QDesktopServices::openUrl(link);
+    } else {
+        emit linkActivated(link);
+    }
+}
+
+/*
+ * Signals
+ */
+/*! \fn void Feed::ready()
+ * This signal is emitted when a updates are available and the UpdateDialog is
+ * ready to be shown with show() or exec().
+ */
+
+/*! \fn void Feed::installButtonClicked(QAbstractButton* button, QString filePath)
+ * This signal is emitted when a custom install button was clicked.
+ */
+
+}  // namespace dblsqd
diff --git a/src/dblsqd/update_dialog.h b/src/dblsqd/update_dialog.h
new file mode 100644 (file)
index 0000000..ce49ea8
--- /dev/null
@@ -0,0 +1,106 @@
+#ifndef DBLSQD_UPDATE_DIALOG_H
+#define DBLSQD_UPDATE_DIALOG_H
+
+#include <QDesktopServices>
+#include <QFile>
+#include <QMessageBox>
+#include <QSettings>
+#include <QTemporaryFile>
+
+#include "feed.h"
+#include "ui_update_dialog.h"
+
+namespace dblsqd
+{
+
+class UpdateDialog : public QDialog
+{
+    Q_OBJECT
+
+   public:
+    enum Type { OnUpdateAvailable, OnLastWindowClosed, Manual, ManualChangelog };
+    explicit UpdateDialog(Feed* feed, int = OnUpdateAvailable, QWidget* parent = 0,
+                          QSettings* settings = new QSettings());
+    ~UpdateDialog();
+
+    void setIcon(QString fileName);
+    void setIcon(QPixmap pixmap);
+    void addInstallButton(QAbstractButton* button);
+
+    void setMinVersion(QString version);
+    void setMaxVersion(QString version);
+    void setPreviousVersion(QString version);
+
+    static bool autoDownloadEnabled(QVariant defaultValue,
+                                    QSettings* settings = new QSettings);
+    static bool autoDownloadEnabled(QSettings* settings = new QSettings());
+    static void enableAutoDownload(bool enabled, QSettings* settings = new QSettings);
+
+    void setOpenExternalLinks(bool open);
+    bool openExternalLinks();
+
+   signals:
+    void ready();
+    void installButtonClicked(QAbstractButton* button, QString filePath);
+    void linkActivated(QString link);
+
+   public slots:
+    void onButtonInstall();
+    void onButtonCustomInstall();
+    void skip();
+    void showIfUpdatesAvailable();
+    void showIfUpdatesAvailableOrQuit();
+
+   private:
+    Ui::UpdateDialog* ui;
+    Feed* feed;
+    int type;
+
+    QSettings* settings;
+    void replaceAppVars(QString& string);
+    QString generateChangelogDocument();
+
+    void disableButtons(bool disable = true);
+    void resetUi();
+    void setupLoadingUi();
+    void setupUpdateUi();
+    void setupChangelogUi();
+    void setupNoUpdatesUi();
+    void adjustDialogSize();
+
+    void startDownload();
+    virtual void startUpdate();
+
+    bool accepted;
+    bool isDownloadFinished;
+    QString updateFilePath;
+    QList<Release> releases;
+    QList<Release> updates;
+    Release latestRelease;
+    QList<QAbstractButton*> installButtons;
+    QAbstractButton* acceptedInstallButton;
+    bool _openExternalLinks;
+    QString _minVersion;
+    QString _maxVersion;
+    QString _previousVersion;
+
+    static void setSettingsValue(QString key, QVariant value,
+                                 QSettings* settings = new QSettings());
+    static QVariant settingsValue(QString key, QVariant defaultValue = QVariant(),
+                                  QSettings* settings = new QSettings());
+    static void removeSetting(QString key, QSettings* settings = new QSettings());
+    static void setDefaultSettingsValue(QString key, QVariant value,
+                                        QSettings* settings = new QSettings());
+
+   private slots:
+    void handleFeedReady();
+    void handleDownloadFinished();
+    void handleDownloadError(QString);
+    void updateProgressBar(qint64, qint64);
+    void autoDownloadCheckboxToggled(bool enabled = true);
+    void onLinkActivated(QString link);
+};
+
+}  // namespace dblsqd
+
+#endif  // DBLSQD_UPDATE_DIALOG_H
diff --git a/src/dblsqd/update_dialog.ui b/src/dblsqd/update_dialog.ui
new file mode 100644 (file)
index 0000000..8bb40e4
--- /dev/null
@@ -0,0 +1,402 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>UpdateDialog</class>
+ <widget class="QDialog" name="UpdateDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>645</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>600</width>
+    <height>0</height>
+   </size>
+  </property>
+  <property name="modal">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <item>
+    <widget class="QWidget" name="headerContainerLoading" native="true">
+     <layout class="QGridLayout" name="gridLayout_4">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>6</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+      <item row="0" column="0">
+       <widget class="QLabel" name="labelHeadlineLoading">
+        <property name="font">
+         <font>
+          <weight>75</weight>
+          <bold>true</bold>
+         </font>
+        </property>
+        <property name="text">
+         <string>Loading update information …</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QWidget" name="headerContainer" native="true">
+     <layout class="QGridLayout" name="gridLayout">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>6</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>18</number>
+      </property>
+      <property name="horizontalSpacing">
+       <number>24</number>
+      </property>
+      <property name="verticalSpacing">
+       <number>6</number>
+      </property>
+      <item row="0" column="2">
+       <widget class="QLabel" name="labelHeadline">
+        <property name="font">
+         <font>
+          <weight>75</weight>
+          <bold>true</bold>
+         </font>
+        </property>
+        <property name="text">
+         <string>A new version of %APPNAME% is available!</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="0" rowspan="3">
+       <widget class="QLabel" name="labelIcon">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="2" rowspan="2">
+       <widget class="QLabel" name="labelInfo">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="text">
+         <string>%APPNAME% %UPDATE_VERSION% is available (you have %CURRENT_VERSION%).
+Would you like to update now?</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QWidget" name="headerContainerChangelog" native="true">
+     <layout class="QGridLayout" name="gridLayout_3">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>6</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>18</number>
+      </property>
+      <item row="0" column="0">
+       <widget class="QLabel" name="labelHeadlineChangelog">
+        <property name="font">
+         <font>
+          <weight>75</weight>
+          <bold>true</bold>
+         </font>
+        </property>
+        <property name="text">
+         <string>Changelog for %APPNAME%</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QLabel" name="labelInfoChangelog">
+        <property name="text">
+         <string>You are using version %CURRENT_VERSION%.</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QWidget" name="headerContainerNoUpdates" native="true">
+     <layout class="QGridLayout" name="gridLayout_2">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>6</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>18</number>
+      </property>
+      <property name="horizontalSpacing">
+       <number>24</number>
+      </property>
+      <item row="1" column="0">
+       <widget class="QLabel" name="labelInfoNoUpdates">
+        <property name="text">
+         <string>There are currently no updates available.</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="0">
+       <widget class="QLabel" name="labelHeadlineNoUpdates">
+        <property name="font">
+         <font>
+          <weight>75</weight>
+          <bold>true</bold>
+         </font>
+        </property>
+        <property name="text">
+         <string>You are using %APPNAME% %CURRENT_VERSION%.</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QScrollArea" name="scrollAreaChangelog">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>0</width>
+       <height>150</height>
+      </size>
+     </property>
+     <property name="widgetResizable">
+      <bool>true</bool>
+     </property>
+     <widget class="QWidget" name="scrollAreaWidgetContents">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>0</y>
+        <width>580</width>
+        <height>235</height>
+       </rect>
+      </property>
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <property name="styleSheet">
+       <string notr="true">background:white;</string>
+      </property>
+      <layout class="QVBoxLayout" name="verticalLayout">
+       <property name="leftMargin">
+        <number>0</number>
+       </property>
+       <property name="topMargin">
+        <number>0</number>
+       </property>
+       <property name="rightMargin">
+        <number>0</number>
+       </property>
+       <property name="bottomMargin">
+        <number>0</number>
+       </property>
+       <item>
+        <widget class="QLabel" name="labelChangelog">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="textFormat">
+          <enum>Qt::RichText</enum>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+         <property name="margin">
+          <number>5</number>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <spacer name="verticalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeType">
+      <enum>QSizePolicy::Fixed</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>20</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item>
+    <widget class="QProgressBar" name="progressBar">
+     <property name="value">
+      <number>24</number>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QCheckBox" name="checkAutoDownload">
+     <property name="text">
+      <string>Automatically download future updates</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QWidget" name="buttonContainer" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout">
+      <property name="spacing">
+       <number>12</number>
+      </property>
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>0</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+      <item>
+       <widget class="QToolButton" name="buttonCancel">
+        <property name="contextMenuPolicy">
+         <enum>Qt::ActionsContextMenu</enum>
+        </property>
+        <property name="popupMode">
+         <enum>QToolButton::MenuButtonPopup</enum>
+        </property>
+        <property name="toolButtonStyle">
+         <enum>Qt::ToolButtonFollowStyle</enum>
+        </property>
+        <property name="autoRaise">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="buttonCancelLoading">
+        <property name="text">
+         <string>Cancel</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <widget class="QPushButton" name="buttonInstall">
+        <property name="text">
+         <string>Install update now</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="buttonConfirm">
+        <property name="text">
+         <string>OK</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+  <action name="actionCancel">
+   <property name="text">
+    <string>Remind me later</string>
+   </property>
+  </action>
+  <action name="actionSkip">
+   <property name="text">
+    <string>Skip this version</string>
+   </property>
+  </action>
+ </widget>
+ <layoutdefault spacing="6" margin="11"/>
+ <tabstops>
+  <tabstop>checkAutoDownload</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/gui/Browse.qml b/src/gui/Browse.qml
new file mode 100644 (file)
index 0000000..1eea79f
--- /dev/null
@@ -0,0 +1,372 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtGraphicalEffects 1.12
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    Rectangle {
+        width: parent.width; height: parent.height
+        color: backgroundColour
+    }
+    
+    property bool refreshing: false
+    
+    property int buttonHeight: 25
+    property int buttonWidth: 103
+    property int fontMedium: 11
+    
+    property int scrollY: 0
+    
+    property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
+    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+    
+    function refresh() {
+        scrollY = studioListView.contentY;
+        var currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY);
+        if (currentIndex == -1) {
+            currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY + (16 * virtualstudio.uiScale));
+        }
+        virtualstudio.refreshStudios(currentIndex)
+    }
+    
+    Rectangle {
+        z: 1
+        width: parent.width; height: parent.height
+        color: "#40000000"
+        visible: refreshing
+        MouseArea {
+            anchors.fill: parent
+            propagateComposedEvents: false
+            hoverEnabled: true
+            preventStealing: true
+        }
+    }
+    
+    Component {
+        id: sectionHeading
+        Rectangle {
+            color: "transparent"
+            height: 72 * virtualstudio.uiScale; x: 16 * virtualstudio.uiScale; width: ListView.view.width - (2 * x)
+            // required property string section: section (for 5.15)
+            Text {
+                id: sectionText
+                //anchors.bottom: parent.bottom
+                y: 12 * virtualstudio.uiScale
+                // text: parent.section (for 5.15)
+                text: section
+                font { family: "Poppins"; pixelSize: 28 * virtualstudio.fontScale * virtualstudio.uiScale; weight: Font.Bold }
+                color: textColour
+            }
+            Button {
+                id: createButton
+                background: Rectangle {
+                    radius: 6 * virtualstudio.uiScale
+                    color: createButton.down ? "#E7E8E8" : "#F2F3F3"
+                    border.width: 1
+                    border.color: createButton.down ? "#B0B5B5" : "#EAEBEB"
+                    layer.enabled: createButton.hovered && !createButton.down
+                    layer.effect: DropShadow {
+                        horizontalOffset: 1 * virtualstudio.uiScale
+                        verticalOffset: 1 * virtualstudio.uiScale
+                        radius: 8.0 * virtualstudio.uiScale
+                        samples: 17
+                        color: "#80A1A1A1"
+                    }
+                }
+                onClicked: { virtualstudio.createStudio(); }
+                anchors.right: filterButton.left
+                anchors.rightMargin: 16
+                anchors.verticalCenter: sectionText.verticalCenter
+                width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+                Text {
+                    text: "Create a Studio"
+                    font.family: "Poppins"
+                    font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                    font.weight: Font.Bold
+                    color: "#DB0A0A"
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.verticalCenter: parent.verticalCenter
+                }
+                visible: section == virtualstudio.logoSection ? true : false
+            }
+            Button {
+                id: filterButton
+                background: Rectangle {
+                    radius: 6 * virtualstudio.uiScale
+                    color: filterButton.down ? "#E7E8E8" : "#F2F3F3"
+                    border.width: 1
+                    border.color: filterButton.down ? "#B0B5B5" : "#EAEBEB"
+                    layer.enabled: filterButton.hovered && !filterButton.down
+                    layer.effect: DropShadow {
+                        horizontalOffset: 1 * virtualstudio.uiScale
+                        verticalOffset: 1 * virtualstudio.uiScale
+                        radius: 8.0 * virtualstudio.uiScale
+                        samples: 17
+                        color: "#80A1A1A1"
+                    }
+                }
+                onClicked: { filterMenu.open(); }
+                anchors.right: parent.right
+                anchors.verticalCenter: sectionText.verticalCenter
+                width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+                Text {
+                    text: "Filter Studios"
+                    font.family: "Poppins"
+                    font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.verticalCenter: parent.verticalCenter
+                }
+                visible: section == virtualstudio.logoSection ? true : false
+
+                Popup {
+                    id: filterMenu
+                    y: Math.round(parent.height + 8)
+                    rightMargin: 16 * virtualstudio.uiScale
+                    width: 210 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale
+                    modal: false
+                    focus: false
+                    closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+                    background: Rectangle {
+                        radius: 6 * virtualstudio.uiScale
+                        color: "#F6F8F8"
+                        border.width: 1
+                        border.color: "#34979797"
+                        layer.enabled: true
+                        layer.effect: DropShadow {
+                            horizontalOffset: 1 * virtualstudio.uiScale
+                            verticalOffset: 1 * virtualstudio.uiScale
+                            radius: 8.0 * virtualstudio.uiScale
+                            samples: 17
+                            color: "#80A1A1A1"
+                        }
+                    }
+                    contentItem: Column {
+                        anchors.fill: parent
+                        CheckBox {
+                            id: inactiveCheckbox
+                            text: qsTr("Show my inactive Studios")
+                            checkState: virtualstudio.showInactive ? Qt.Checked : Qt.Unchecked
+                            onClicked: { virtualstudio.showInactive = inactiveCheckbox.checkState == Qt.Checked;
+                                refreshing = true;
+                                refresh();
+                            }
+                            indicator: Rectangle {
+                                implicitWidth: 16 * virtualstudio.uiScale
+                                implicitHeight: 16 * virtualstudio.uiScale
+                                x: inactiveCheckbox.leftPadding
+                                y: parent.height / 2 - height / 2
+                                radius: 3 * virtualstudio.uiScale
+                                border.color: inactiveCheckbox.down ? "#007AFF" : "#0062cc"
+
+                                Rectangle {
+                                    width: 10 * virtualstudio.uiScale
+                                    height: 10 * virtualstudio.uiScale
+                                    x: 3 * virtualstudio.uiScale
+                                    y: 3 * virtualstudio.uiScale
+                                    radius: 2 * virtualstudio.uiScale
+                                    color: inactiveCheckbox.down ? "#007AFF" : "#0062cc"
+                                    visible: inactiveCheckbox.checked
+                                }
+                            }
+                            contentItem: Text {
+                                text: inactiveCheckbox.text
+                                font.family: "Poppins"
+                                font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+                                anchors.horizontalCenter: parent.horizontalCenter
+                                anchors.verticalCenter: parent.verticalCenter
+                                leftPadding: inactiveCheckbox.indicator.width + inactiveCheckbox.spacing
+                            }
+                        }
+                        CheckBox {
+                            id: selfHostedCheckbox
+                            text: qsTr("Show self-hosted Studios")
+                            checkState: virtualstudio.showSelfHosted ? Qt.Checked : Qt.Unchecked
+                            onClicked: { virtualstudio.showSelfHosted = selfHostedCheckbox.checkState == Qt.Checked;
+                                refreshing = true;
+                                refresh();
+                            }
+                            indicator: Rectangle {
+                                implicitWidth: 16 * virtualstudio.uiScale
+                                implicitHeight: 16 * virtualstudio.uiScale
+                                x: selfHostedCheckbox.leftPadding
+                                y: parent.height / 2 - height / 2
+                                radius: 3 * virtualstudio.uiScale
+                                border.color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC"
+
+                                Rectangle {
+                                    width: 10 * virtualstudio.uiScale
+                                    height: 10 * virtualstudio.uiScale
+                                    x: 3 * virtualstudio.uiScale
+                                    y: 3 * virtualstudio.uiScale
+                                    radius: 2 * virtualstudio.uiScale
+                                    color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC"
+                                    visible: selfHostedCheckbox.checked
+                                }
+                            }
+                            contentItem: Text {
+                                text: selfHostedCheckbox.text
+                                font.family: "Poppins"
+                                font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+                                anchors.horizontalCenter: parent.horizontalCenter
+                                anchors.verticalCenter: parent.verticalCenter
+                                leftPadding: selfHostedCheckbox.indicator.width + selfHostedCheckbox.spacing
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    
+    Component {
+        id: footer
+        Rectangle {
+            height: 16 * virtualstudio.uiScale
+            x: 16 * virtualstudio.uiScale
+            width: parent.width - (2 * x)
+            color: backgroundColour
+        }
+    }
+
+    ListView {
+        id: studioListView
+        x:0; y: 0; width: parent.width - (2 * x); height: parent.height - 36 * virtualstudio.uiScale
+        spacing: 16 * virtualstudio.uiScale
+        header: footer
+        footer: footer
+        model: serverModel
+        clip: true
+        boundsBehavior: Flickable.StopAtBounds
+        delegate: Studio {
+            x: 16 * virtualstudio.uiScale
+            width: studioListView.width - (2 * x)
+            serverLocation: location
+            flagImage: flag
+            studioName: name
+            publicStudio: isPublic
+            manageable: isManageable
+            available: canConnect
+            connected: false
+        }
+        
+        section {property: "type"; criteria: ViewSection.FullString; delegate: sectionHeading }
+
+        // Disable momentum scroll
+        MouseArea {
+            z: -1
+            anchors.fill: parent
+            onWheel: {
+                // trackpad
+                studioListView.contentY -= wheel.pixelDelta.y;
+                // mouse wheel
+                studioListView.contentY -= wheel.angleDelta.y;
+                studioListView.returnToBounds();
+            }
+        }
+        
+        Component.onCompleted: {
+            // Customize scroll properties on different platforms
+            if (Qt.platform.os == "linux" || Qt.platform.os == "osx" ||
+                Qt.platform.os == "unix" || Qt.platform.os == "windows") {
+                var scrollBar = Qt.createQmlObject('import QtQuick.Controls 2.12; ScrollBar{}',
+                                                   studioListView,
+                                                   "dynamicSnippet1");
+                scrollBar.policy = ScrollBar.AlwaysOn;
+                ScrollBar.vertical = scrollBar;
+            }
+        }
+    }
+    
+    Rectangle {
+        x: 0; y: parent.height - 36 * virtualstudio.uiScale; width: parent.width; height: 36 * virtualstudio.uiScale
+        border.color: "#33979797"
+        color: backgroundColour
+        
+        Button {
+            id: refreshButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: { refreshing = true; refresh() }
+            anchors.verticalCenter: parent.verticalCenter
+            x: 16 * virtualstudio.uiScale
+            width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+            Text {
+                text: "Refresh List"
+                font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+        
+        Button {
+            id: aboutButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: aboutButton.down ? buttonPressedColour : (aboutButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: aboutButton.down ? buttonPressedStroke : (aboutButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: { virtualstudio.showAbout() }
+            anchors.verticalCenter: parent.verticalCenter
+            x: parent.width - (230 * virtualstudio.uiScale)
+            width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+            Text {
+                text: "About"
+                font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+        
+        Button {
+            id: settingsButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: settingsButton.down ? buttonPressedColour : (settingsButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: settingsButton.down ? buttonPressedStroke : (settingsButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: window.state = "settings"
+            anchors.verticalCenter: parent.verticalCenter
+            x: parent.width - (119 * virtualstudio.uiScale)
+            width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+            Text {
+                text: "Settings"
+                font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
+                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+    }
+    
+    Connections {
+        target: virtualstudio
+        // Need to do this to avoid layout issues with our section header.
+        function onNewScale() { 
+            studioListView.positionViewAtEnd();
+            studioListView.positionViewAtBeginning();
+            scrollY = studioListView.contentY;
+        }
+        function onRefreshFinished(index) {
+            refreshing = false;
+            if (index == -1) {
+                studioListView.contentY = scrollY
+            } else {
+                studioListView.positionViewAtIndex(index, ListView.Beginning);
+            }
+        }
+        function onPeriodicRefresh() { refresh() }
+    }
+}
diff --git a/src/gui/Connected.qml b/src/gui/Connected.qml
new file mode 100644 (file)
index 0000000..b449c46
--- /dev/null
@@ -0,0 +1,94 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtGraphicalEffects 1.12
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+    
+    property bool connecting: false
+    
+    property int leftMargin: 16
+    property int fontBig: 28
+    property int fontMedium: 18
+    
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property real imageLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0
+
+    Image {
+        x: parent.width - (49 * virtualstudio.uiScale); y: 16 * virtualstudio.uiScale
+        width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale
+        source: "logo.svg"
+    }
+    
+    Text {
+        id: heading
+        text: virtualstudio.connectionState
+        x: leftMargin * virtualstudio.uiScale; y: 34 * virtualstudio.uiScale
+        font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
+        color: textColour
+    }
+    
+    Studio {
+        x: parent.leftMargin * virtualstudio.uiScale; y: 96 * virtualstudio.uiScale
+        width: parent.width - (2 * x)
+        connected: true
+        serverLocation: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].location : "Germany - Berlin"
+        flagImage: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].flag : "flags/DE.svg"
+        studioName: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].name : "Test Studio"
+        publicStudio: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].isPublic : false
+        manageable: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].isManageable : false
+        available: virtualstudio.currentStudio >= 0 ? serverModel[virtualstudio.currentStudio].canConnect : false
+    }
+    
+    Image {
+        id: mic
+        source: "mic.svg"
+        x: 80 * virtualstudio.uiScale; y: 250 * virtualstudio.uiScale
+        width: 18 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale
+    }
+
+    Colorize {
+        anchors.fill: mic
+        source: mic
+        hue: 0
+        saturation: 0
+        lightness: imageLightnessValue
+    }
+    
+    Image {
+        id: headphones
+        source: "headphones.svg"
+        anchors.horizontalCenter: mic.horizontalCenter
+        y: 329 * virtualstudio.uiScale
+        width: 24 * virtualstudio.uiScale; height: 26 * virtualstudio.uiScale
+    }
+
+    Colorize {
+        anchors.fill: headphones
+        source: headphones
+        hue: 0
+        saturation: 0
+        lightness: imageLightnessValue
+    }
+    
+    Text {
+        x: 120 * virtualstudio.uiScale
+        text: virtualstudio.audioBackend == "JACK" ? 
+            virtualstudio.audioBackend : inputComboModel[virtualstudio.inputDevice]
+        font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        anchors.verticalCenter: mic.verticalCenter
+        color: textColour
+    }
+    
+    Text {
+        x: 120 * virtualstudio.uiScale
+        text: virtualstudio.audioBackend == "JACK" ? 
+            virtualstudio.audioBackend : outputComboModel[virtualstudio.outputDevice]
+        font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        anchors.verticalCenter: headphones.verticalCenter
+        color: textColour
+    }
+    
+    //43 822
+}
diff --git a/src/gui/FirstLaunch.qml b/src/gui/FirstLaunch.qml
new file mode 100644 (file)
index 0000000..7356df2
--- /dev/null
@@ -0,0 +1,139 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtGraphicalEffects 1.12
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+    
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1"
+    property string buttonColour: virtualstudio.darkMode ? "#565252" : "#F0F1F1"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#6F6C6C" : "#F0F1F1"
+    property string buttonPressedColour: virtualstudio.darkMode ? "#494646" : "#D8D9D9"
+    property string buttonStroke: virtualstudio.darkMode ? "#636060" : "#DEDFDF"
+    property string buttonHoverStroke: virtualstudio.darkMode ? "#777575" : "#DEDFDF"
+    property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5"
+    
+    Image {
+        source: "logo.svg"
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: 35 * virtualstudio.uiScale
+        width: 50 * virtualstudio.uiScale; height: 92 * virtualstudio.uiScale
+    }
+
+    Text {
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: 168 * virtualstudio.uiScale
+        text: "Sign in with a Virtual Studio account?"
+        font.family: "Poppins"
+        font.pixelSize: 17 * virtualstudio.fontScale * virtualstudio.uiScale
+        color: textColour
+    }
+
+    Text {
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: 219 * virtualstudio.uiScale
+        text: "You'll be able to change your mind later"
+        font.family: "Poppins"
+        font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+        color: textColour
+    }
+
+    Button {
+        id: vsButton
+        background: Rectangle {
+            radius: 10 * virtualstudio.uiScale
+            color: vsButton.down ? buttonPressedColour : (vsButton.hovered ? buttonHoverColour : buttonColour)
+            border.width: 1
+            border.color: vsButton.down ? buttonPressedStroke : (vsButton.hovered ? buttonHoverStroke : buttonStroke)
+            layer.enabled: vsButton.hovered && !vsButton.down
+            layer.effect: DropShadow {
+                horizontalOffset: 1 * virtualstudio.uiScale
+                verticalOffset: 1 * virtualstudio.uiScale
+                radius: 8.0 * virtualstudio.uiScale
+                samples: 17
+                color: shadowColour
+            }
+        }
+        onClicked: { window.state = "login"; virtualstudio.toVirtualStudio(); }
+        x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale
+        width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale
+        Text {
+            text: "Yes"
+            font.family: "Poppins"
+            font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
+            font.weight: Font.Bold
+            color: "#DB0A0A"
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.verticalCenter: parent.verticalCenter
+        }
+    }
+    Text {
+        text: "• Connect to Virtual Studios<br>• Broadcast on JackTrip Radio<br>• Apply FX with Soundscapes"
+        textFormat: Text.StyledText
+        font.family: "Poppins"
+        font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+        x: parent.width / 2 - (265 * virtualstudio.uiScale);
+        y: 355 * virtualstudio.uiScale;
+        width: 230 * virtualstudio.uiScale
+        padding: 0
+        wrapMode: Text.WordWrap
+        horizontalAlignment: Text.AlignHCenter
+        color: textColour
+    }
+    Image {
+        source: "JTVS.png"
+        x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale
+        width: 234 * virtualstudio.uiScale; height: 201.48 * virtualstudio.uiScale;
+    }
+
+    Button {
+        id: standardButton
+        background: Rectangle {
+            radius: 10 * virtualstudio.uiScale
+            color: standardButton.down ? buttonPressedColour : (standardButton.hovered ? buttonHoverColour : buttonColour)
+            border.width: 1
+            border.color: standardButton.down ? buttonPressedStroke : (standardButton.hovered ? buttonHoverStroke : buttonStroke)
+            layer.enabled: standardButton.hovered && !standardButton.down
+            layer.effect: DropShadow {
+                horizontalOffset: 1 * virtualstudio.uiScale
+                verticalOffset: 1 * virtualstudio.uiScale
+                radius: 8.0 * virtualstudio.uiScale
+                samples: 17
+                color: shadowColour
+            }
+        }
+        onClicked: { window.state = "login"; virtualstudio.toStandard(); }
+        x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale
+        width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale
+        Text {
+            text: "No"
+            font.family: "Poppins"
+            font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
+            font.weight: Font.Bold
+            color: "#DB0A0A"
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.verticalCenter: parent.verticalCenter
+        }
+    }
+    Image {
+        source: "JTOriginal.png"
+        x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale
+        width: 234 * virtualstudio.uiScale; height: 337.37 * virtualstudio.uiScale;
+    }
+    Text {
+        text: virtualstudio.psiBuild ? "• Connect via IP address<br>• Run a local hub server<br>• The Standard JackTrip experience" :
+              "• Connect via IP address<br>• Run a local hub server<br>• The Classic JackTrip experience"
+        textFormat: Text.StyledText
+        font.family: "Poppins"
+        font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+        x: parent.width / 2 + (32 * virtualstudio.uiScale);
+        y: 355 * virtualstudio.uiScale;
+        width: 230 * virtualstudio.uiScale
+        padding: 0
+        wrapMode: Text.WordWrap
+        horizontalAlignment: Text.AlignHCenter
+        color: textColour
+    }
+}
diff --git a/src/gui/JTOriginal.png b/src/gui/JTOriginal.png
new file mode 100644 (file)
index 0000000..a6c92cd
Binary files /dev/null and b/src/gui/JTOriginal.png differ
diff --git a/src/gui/JTVS.png b/src/gui/JTVS.png
new file mode 100644 (file)
index 0000000..bbd5c1d
Binary files /dev/null and b/src/gui/JTVS.png differ
diff --git a/src/gui/Login.qml b/src/gui/Login.qml
new file mode 100644 (file)
index 0000000..0ed3fc0
--- /dev/null
@@ -0,0 +1,141 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtGraphicalEffects 1.12
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+    
+    Rectangle {
+        width: parent.width; height: parent.height
+        color: backgroundColour
+    }
+
+    property bool failTextVisible: false
+    
+    property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5"
+    property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5"
+    property string buttonStroke: virtualstudio.darkMode ? "#9C9C9C" : "#A4A7A7"
+    property string buttonTextColour: virtualstudio.darkMode ? "#272525" : "#DB0A0A"
+    property string buttonTextHover: virtualstudio.darkMode ? "#242222" : "#D00A0A"
+    property string buttonTextPressed: virtualstudio.darkMode ? "#323030" : "#D00A0A"
+    property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1"
+    
+    onFailTextVisibleChanged: {
+        authFailedText.visible = failTextVisible;
+        loginButton.visible = failTextVisible || !virtualstudio.hasRefreshToken;
+        backButton.visible = failTextVisible || !virtualstudio.hasRefreshToken;
+        loggingInText.visible = !failTextVisible && virtualstudio.hasRefreshToken;
+    }
+    
+    Image {
+        id: loginLogo
+        source: "logo.svg"
+        x: parent.width / 2 - (150 * virtualstudio.uiScale); y: 110 * virtualstudio.uiScale
+        width: 42 * virtualstudio.uiScale; height: 76 * virtualstudio.uiScale
+    }
+
+    Image {
+        source: virtualstudio.darkMode ? "jacktrip white.png" : "jacktrip.png"
+        anchors.bottom: loginLogo.bottom
+        x: parent.width / 2 - (88 * virtualstudio.uiScale)
+        width: 238 * virtualstudio.uiScale; height: 56 * virtualstudio.uiScale
+    }
+
+    Text {
+        text: "Virtual Studio"
+        font.family: "Poppins"
+        font.pixelSize: 28 * virtualstudio.fontScale * virtualstudio.uiScale
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: 208 * virtualstudio.uiScale
+        color: textColour
+    }
+
+    Text {
+        id: loggingInText
+        text: "Logging in..."
+        font.family: "Poppins"
+        font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: 282 * virtualstudio.uiScale
+        visible: virtualstudio.hasRefreshToken
+        color: textColour
+    }
+
+    Text {
+        id: authFailedText
+        text: "Log in failed. Please try again."
+        font.family: "Poppins"
+        font.pixelSize: 16 * virtualstudio.fontScale * virtualstudio.uiScale
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: 272 * virtualstudio.uiScale
+        visible: failTextVisible
+        color: textColour
+    }
+
+    Button {
+        id: loginButton
+        background: Rectangle {
+            radius: 6 * virtualstudio.uiScale
+            color: loginButton.down ? buttonPressedColour : (loginButton.hovered ? buttonHoverColour : buttonColour)
+            border.width: loginButton.down ? 1 : 0
+            border.color: buttonStroke
+            layer.enabled: !loginButton.down
+            layer.effect: DropShadow {
+                horizontalOffset: 1 * virtualstudio.uiScale
+                verticalOffset: 1 * virtualstudio.uiScale
+                radius: 8.0 * virtualstudio.uiScale
+                samples: 17
+                color: shadowColour
+            }
+        }
+        onClicked: { failTextVisible = false; virtualstudio.login() }
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: 321 * virtualstudio.uiScale
+        width: 263 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale
+        Text {
+            text: "Sign In"
+            font.family: "Poppins"
+            font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
+            font.weight: Font.Bold
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.verticalCenter: parent.verticalCenter
+            color: loginButton.down ? buttonTextPressed : (loginButton.hovered ? buttonTextHover : buttonTextColour)
+        }
+        visible: !virtualstudio.hasRefreshToken
+    }
+
+    Button {
+        id: backButton
+        background: Rectangle {
+            radius: 6 * virtualstudio.uiScale
+            color: backButton.down ? buttonPressedColour : (backButton.hovered ? buttonHoverColour : buttonColour)
+            border.width: backButton.down ? 1 : 0
+            border.color: buttonStroke
+            layer.enabled: !backButton.down
+            layer.effect: DropShadow {
+                horizontalOffset: 1 * virtualstudio.uiScale
+                verticalOffset: 1 * virtualstudio.uiScale
+                radius: 8.0 * virtualstudio.uiScale
+                samples: 17
+                color: shadowColour
+            }
+        }
+        onClicked: { window.state = "start" }
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: 401 * virtualstudio.uiScale
+        width: 263 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale
+        Text {
+            text: "Back"
+            font.family: "Poppins"
+            font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.verticalCenter: parent.verticalCenter
+            color: backButton.down ? buttonTextPressed : (backButton.hovered ? buttonTextHover : buttonTextColour)
+        }
+        visible: true
+    }
+}
diff --git a/src/gui/Poppins-Bold.ttf b/src/gui/Poppins-Bold.ttf
new file mode 100644 (file)
index 0000000..00559ee
Binary files /dev/null and b/src/gui/Poppins-Bold.ttf differ
diff --git a/src/gui/Poppins-Regular.ttf b/src/gui/Poppins-Regular.ttf
new file mode 100644 (file)
index 0000000..9f0c71b
Binary files /dev/null and b/src/gui/Poppins-Regular.ttf differ
diff --git a/src/gui/Settings.qml b/src/gui/Settings.qml
new file mode 100644 (file)
index 0000000..0ba2ed8
--- /dev/null
@@ -0,0 +1,318 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+    
+    Rectangle {
+        width: parent.width; height: parent.height
+        color: backgroundColour
+    }
+
+    property int fontBig: 28
+    property int fontMedium: 13
+    property int fontSmall: 11
+    
+    property int leftMargin: 48
+    property int buttonWidth: 103
+    property int buttonHeight: 25
+    
+    property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
+    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+    
+    Text {
+        x: 16 * virtualstudio.uiScale; y: 32 * virtualstudio.uiScale
+        text: "Settings"
+        font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
+        color: textColour
+    }
+    
+    ComboBox {
+        id: backendCombo
+        model: backendComboModel
+        currentIndex: virtualstudio.audioBackend == "JACK" ? 0 : 1
+        onActivated: { virtualstudio.audioBackend = currentText }
+        x: 234 * virtualstudio.uiScale; y: 100 * virtualstudio.uiScale
+        width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale
+        visible: virtualstudio.selectableBackend
+    }
+    
+    Text {
+        id: backendLabel
+        anchors.verticalCenter: backendCombo.verticalCenter
+        x: leftMargin * virtualstudio.uiScale
+        text: "Audio Backend"
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        visible: virtualstudio.selectableBackend
+        color: textColour
+    }
+    
+    Text {
+        id: jackLabel
+        x: leftMargin * virtualstudio.uiScale; y: 100 * virtualstudio.uiScale
+        width: parent.width - x - (16 * virtualstudio.uiScale)
+        text: "Using JACK for audio input and output. Use QjackCtl to adjust your sample rate, buffer, and device settings."
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        wrapMode: Text.WordWrap
+        visible: virtualstudio.audioBackend == "JACK" && !virtualstudio.selectableBackend
+        color: textColour
+    }
+    
+    ComboBox {
+        id: inputCombo
+        model: inputComboModel
+        currentIndex: virtualstudio.inputDevice
+        onActivated: { virtualstudio.inputDevice = currentIndex }
+        x: 234 * virtualstudio.uiScale; y: virtualstudio.uiScale * (virtualstudio.selectableBackend ? 148 : 100)
+        width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale
+        visible: virtualstudio.audioBackend != "JACK"
+    }
+    
+    ComboBox {
+        id: outputCombo
+        model: outputComboModel
+        currentIndex: virtualstudio.outputDevice
+        onActivated: { virtualstudio.outputDevice = currentIndex }
+        x: backendCombo.x; y: inputCombo.y + (48 * virtualstudio.uiScale)
+        width: backendCombo.width; height: backendCombo.height
+        visible: virtualstudio.audioBackend != "JACK"
+    }
+    
+    Text {
+        anchors.verticalCenter: inputCombo.verticalCenter
+        x: leftMargin * virtualstudio.uiScale
+        text: "Input Device"
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        visible: virtualstudio.audioBackend != "JACK"
+        color: textColour
+    }
+    
+    Text {
+        anchors.verticalCenter: outputCombo.verticalCenter
+        x: leftMargin * virtualstudio.uiScale
+        text: "Output Device"
+        font { family: "Poppins"; pixelSize: 13 * virtualstudio.fontScale * virtualstudio.uiScale }
+        visible: virtualstudio.audioBackend != "JACK"
+        color: textColour
+    }
+
+    Button {
+        id: refreshButton
+        background: Rectangle {
+            radius: 6 * virtualstudio.uiScale
+            color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour)
+            border.width: 1
+            border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke)
+        }
+        onClicked: { virtualstudio.refreshDevices() }
+        x: parent.width - (232 * virtualstudio.uiScale); y: inputCombo.y + (100 * virtualstudio.uiScale)
+        width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+        visible: virtualstudio.audioBackend != "JACK"
+        Text {
+            text: "Refresh Device List"
+            font { family: "Poppins"; pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale }
+            anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+            color: textColour
+        }
+    }
+    
+    Rectangle {
+        x: leftMargin * virtualstudio.uiScale; y: inputCombo.y + (146 * virtualstudio.uiScale)
+        width: parent.width - x - (16 * virtualstudio.uiScale); height: 1 * virtualstudio.uiScale
+        color: textColour
+        visible: virtualstudio.audioBackend != "JACK"
+    }
+    
+    ComboBox {
+        id: bufferCombo
+        x: backendCombo.x; y: inputCombo.y + (162 * virtualstudio.uiScale)
+        width: backendCombo.width; height: backendCombo.height
+        model: bufferComboModel
+        currentIndex: virtualstudio.bufferSize
+        onActivated: { virtualstudio.bufferSize = currentIndex }
+        font.family: "Poppins"
+        visible: virtualstudio.audioBackend != "JACK"
+    }
+
+    Text {
+        anchors.verticalCenter: bufferCombo.verticalCenter
+        x: 48 * virtualstudio.uiScale
+        text: "Buffer Size"
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        visible: virtualstudio.audioBackend != "JACK"
+        color: textColour
+    }
+    
+    Rectangle {
+        id: separator
+        x: leftMargin * virtualstudio.uiScale
+        width: parent.width - x - (16 * virtualstudio.uiScale); height: 1 * virtualstudio.uiScale
+        y: virtualstudio.audioBackend == "JACK" ? 
+            (virtualstudio.selectableBackend ? backendCombo.y + (48 * virtualstudio.uiScale) : jackLabel.y + (64 * virtualstudio.uiScale)) : bufferCombo.y + (52 * virtualstudio.uiScale) 
+        color: textColour
+    }
+    
+    Slider {
+        id: scaleSlider
+        x: backendCombo.x; y: separator.y + (16 * virtualstudio.uiScale)
+        width: backendCombo.width
+        from: 1; to: 2; value: virtualstudio.uiScale
+        onMoved: { virtualstudio.uiScale = value }
+    }
+    
+    Text {
+        anchors.verticalCenter: scaleSlider.verticalCenter
+        x: leftMargin * virtualstudio.uiScale
+        text: "Scale Interface"
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        color: textColour
+    }
+    
+    Button {
+        id: modeButton
+        background: Rectangle {
+            radius: 6 * virtualstudio.uiScale
+            color: modeButton.down ? buttonPressedColour : (modeButton.hovered ? buttonHoverColour : buttonColour)
+            border.width: 1
+            border.color: modeButton.down ? buttonPressedStroke : (modeButton.hovered ? buttonHoverStroke : buttonStroke)
+        }
+        onClicked: { window.state = "login"; virtualstudio.toStandard(); }
+        x: parent.width - (232 * virtualstudio.uiScale); y: scaleSlider.y + (40 * virtualstudio.uiScale)
+        width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+        Text {
+            text: virtualstudio.psiBuild ? "Switch to Standard Mode" : "Switch to Classic Mode"
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+            color: textColour
+        }
+    }
+    
+    Button {
+        id: darkButton
+        background: Rectangle {
+            radius: 6 * virtualstudio.uiScale
+            color: darkButton.down ? buttonPressedColour : (darkButton.hovered ? buttonHoverColour : buttonColour)
+            border.width: 1
+            border.color: darkButton.down ? buttonPressedStroke : (darkButton.hovered ? buttonHoverStroke : buttonStroke)
+        }
+        onClicked: { virtualstudio.darkMode = !virtualstudio.darkMode; }
+        x: parent.width -(464 * virtualstudio.uiScale)
+        width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+        anchors.verticalCenter: modeButton.verticalCenter
+        Text {
+            text: virtualstudio.darkMode ? "Switch to Light Mode" : "Switch to Dark Mode"
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+            color: textColour
+        }
+    }
+    
+    Text {
+        anchors.verticalCenter: modeButton.verticalCenter
+        x: leftMargin * virtualstudio.uiScale
+        text: "Change Mode"
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        color: textColour
+    }
+
+    ComboBox {
+        id: updateChannelCombo
+        x: parent.width - (232 * virtualstudio.uiScale); y: modeButton.y + (40 * virtualstudio.uiScale)
+        width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+        model: updateChannelComboModel
+        currentIndex: virtualstudio.updateChannel == "stable" ? 0 : 1
+        onActivated: { virtualstudio.updateChannel = currentIndex == 0 ? "stable": "edge" }
+        font.family: "Poppins"
+        visible: !virtualstudio.noUpdater
+    }
+
+    Text {
+        anchors.verticalCenter: updateChannelCombo.verticalCenter
+        x: leftMargin * virtualstudio.uiScale
+        text: "Update Channel"
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        color: textColour
+        visible: !virtualstudio.noUpdater
+    }
+    
+    Text {
+        x: leftMargin * virtualstudio.uiScale; y: parent.height - (75 * virtualstudio.uiScale)
+        text: "JackTrip version " + virtualstudio.versionString
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
+        color: textColour
+    }
+
+    Button {
+        id: logoutButton
+        background: Rectangle {
+            radius: 6 * virtualstudio.uiScale
+            color: logoutButton.down ? buttonPressedColour : (logoutButton.hovered ? buttonHoverColour : buttonColour)
+            border.width: 1
+            border.color: logoutButton.down ? buttonPressedStroke : (logoutButton.hovered ? buttonHoverStroke : buttonStroke)
+        }
+        onClicked: { window.state = "login"; virtualstudio.logout() }
+        x: parent.width - ((16 + buttonWidth) * virtualstudio.uiScale)
+        y: virtualstudio.noUpdater ? modeButton.y + (46 * virtualstudio.uiScale) : updateChannelCombo.y + (46 * virtualstudio.uiScale)
+        width: buttonWidth * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+        Text {
+            text: "Log Out"
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+            color: textColour
+        }
+    }
+
+    Rectangle {
+        x: 0; y: parent.height - (36 * virtualstudio.uiScale)
+        width: parent.width; height: (36 * virtualstudio.uiScale)
+        border.color: "#33979797"
+        color: backgroundColour
+
+        Button {
+            id: cancelButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: cancelButton.down ? buttonPressedColour : (cancelButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: cancelButton.down ? buttonPressedStroke : (cancelButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: { window.state = "browse"; virtualstudio.revertSettings() }
+            anchors.verticalCenter: parent.verticalCenter
+            x: parent.width - (230 * virtualstudio.uiScale)
+            width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+            Text {
+                text: "Cancel"
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+
+        Button {
+            id: saveButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: saveButton.down ? buttonPressedColour : (saveButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: saveButton.down ? buttonPressedStroke : (saveButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: { window.state = "browse"; virtualstudio.applySettings() }
+            anchors.verticalCenter: parent.verticalCenter
+            x: parent.width - (119 * virtualstudio.uiScale)
+            width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+            Text {
+                text: "Save"
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+    }
+}
diff --git a/src/gui/Setup.qml b/src/gui/Setup.qml
new file mode 100644 (file)
index 0000000..d7f53e6
--- /dev/null
@@ -0,0 +1,505 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtGraphicalEffects 1.12
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    property int fontBig: 28
+    property int fontMedium: 13
+    property int fontSmall: 11
+    
+    property int leftMargin: 48
+    property int buttonWidth: 103
+    property int buttonHeight: 25
+
+    property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+    property real imageLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+    property string saveButtonShadow: "#80A1A1A1"
+    property string saveButtonBackgroundColour: "#F2F3F3"
+    property string saveButtonPressedColour: "#E7E8E8"
+    property string saveButtonStroke: "#EAEBEB"
+    property string saveButtonPressedStroke: "#B0B5B5"
+    property string warningText: "#DB0A0A"
+    property string saveButtonText: "#DB0A0A"
+    property string checkboxStroke: "#0062cc"
+    property string checkboxPressedStroke: "#007AFF"
+
+    property bool currShowWarnings: virtualstudio.showWarnings
+    property string warningScreen: virtualstudio.showWarnings ? "ethernet" : "acknowledged"
+
+    Item {
+        id: ethernetWarningItem
+        width: parent.width; height: parent.height
+        visible: warningScreen == "ethernet"
+
+        Image {
+            id: ethernetWarningLogo
+            source: "ethernet.png"
+            width: 179
+            height: 128
+            y: 60
+            anchors.horizontalCenter: parent.horizontalCenter
+        }
+
+        Colorize {
+            anchors.fill: ethernetWarningLogo
+            source: ethernetWarningLogo
+            hue: 0
+            saturation: 0
+            lightness: imageLightnessValue
+        }
+        
+        Text {
+            id: ethernetWarningHeader
+            text: "Connect via Wired Ethernet"
+            font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: ethernetWarningLogo.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: ethernetWarningSubheader1
+            text: "JackTrip works best when you connect directly to your router via wired ethernet."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 400
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: ethernetWarningHeader.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: ethernetWarningSubheader2
+            text: "WiFi works OK for some people, but you will experience higher latency and audio glitches."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 400
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: ethernetWarningSubheader1.bottom
+            anchors.topMargin: 24 * virtualstudio.uiScale
+        }
+
+        Button {
+            id: okButtonEthernet
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: okButtonEthernet.down ? saveButtonPressedColour : saveButtonBackgroundColour
+                border.width: 1
+                border.color: okButtonEthernet.down ? saveButtonPressedStroke : saveButtonStroke
+                layer.enabled: okButtonEthernet.hovered && !okButtonEthernet.down
+                layer.effect: DropShadow {
+                    horizontalOffset: 1 * virtualstudio.uiScale
+                    verticalOffset: 1 * virtualstudio.uiScale
+                    radius: 8.0 * virtualstudio.uiScale
+                    samples: 17
+                    color: saveButtonShadow
+                }
+            }
+            onClicked: { warningScreen = "headphones" }
+            anchors.right: parent.right
+            anchors.rightMargin: 16 * virtualstudio.uiScale
+            anchors.bottomMargin: 16 * virtualstudio.uiScale
+            anchors.bottom: parent.bottom
+            width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+            Text {
+                text: "OK"
+                font.family: "Poppins"
+                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                font.weight: Font.Bold
+                color: saveButtonText
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+            }
+        }
+
+        CheckBox {
+            id: showEthernetWarningCheckbox
+            checked: currShowWarnings
+            text: qsTr("Show warnings again next time")
+            anchors.right: okButtonEthernet.left
+            anchors.rightMargin: 16 * virtualstudio.uiScale
+            anchors.verticalCenter: okButtonEthernet.verticalCenter
+            onClicked: { currShowWarnings = showEthernetWarningCheckbox.checkState == Qt.Checked }
+            indicator: Rectangle {
+                implicitWidth: 16 * virtualstudio.uiScale
+                implicitHeight: 16 * virtualstudio.uiScale
+                x: showEthernetWarningCheckbox.leftPadding
+                y: parent.height / 2 - height / 2
+                radius: 3 * virtualstudio.uiScale
+                border.color: showEthernetWarningCheckbox.down ? checkboxPressedStroke : checkboxStroke
+
+                Rectangle {
+                    width: 10 * virtualstudio.uiScale
+                    height: 10 * virtualstudio.uiScale
+                    x: 3 * virtualstudio.uiScale
+                    y: 3 * virtualstudio.uiScale
+                    radius: 2 * virtualstudio.uiScale
+                    color: showEthernetWarningCheckbox.down ? checkboxPressedStroke : checkboxStroke
+                    visible: showEthernetWarningCheckbox.checked
+                }
+            }
+            contentItem: Text {
+                text: showEthernetWarningCheckbox.text
+                font.family: "Poppins"
+                font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+                leftPadding: showEthernetWarningCheckbox.indicator.width + showEthernetWarningCheckbox.spacing
+                color: textColour
+            }
+        }
+    }
+
+    Item {
+        id: headphoneWarningItem
+        width: parent.width; height: parent.height
+        visible: warningScreen == "headphones"
+
+        Image {
+            id: headphoneWarningLogo
+            source: "headphones.svg"
+            sourceSize: Qt.size( img.sourceSize.width*5, img.sourceSize.height*5 )
+            Image {
+                id: img
+                source: parent.source
+                width: 0
+                height: 0
+            }
+            width: 118
+            height: 128
+            y: 60
+            anchors.horizontalCenter: parent.horizontalCenter
+        }
+
+        Colorize {
+            anchors.fill: headphoneWarningLogo
+            source: headphoneWarningLogo
+            hue: 0
+            saturation: 0
+            lightness: imageLightnessValue
+        }
+        
+        Text {
+            id: headphoneWarningHeader
+            text: "Use Wired Headphones"
+            font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: headphoneWarningLogo.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: headphoneWarningSubheader1
+            text: "JackTrip requires the use of wired headphones."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 400
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: headphoneWarningHeader.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: headphoneWarningSubheader2
+            text: "Using speakers can cause loud feedback loops."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 400
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: headphoneWarningSubheader1.bottom
+            anchors.topMargin: 24 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: headphoneWarningSubheader3
+            text: "Wireless headphones add way too much latency."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 400
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: headphoneWarningSubheader2.bottom
+            anchors.topMargin: 24 * virtualstudio.uiScale
+        }
+
+        Button {
+            id: okButtonHeadphones
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: okButtonHeadphones.down ? saveButtonPressedColour : saveButtonBackgroundColour
+                border.width: 1
+                border.color: okButtonHeadphones.down ? saveButtonPressedStroke : saveButtonStroke
+                layer.enabled: okButtonHeadphones.hovered && !okButtonHeadphones.down
+                layer.effect: DropShadow {
+                    horizontalOffset: 1 * virtualstudio.uiScale
+                    verticalOffset: 1 * virtualstudio.uiScale
+                    radius: 8.0 * virtualstudio.uiScale
+                    samples: 17
+                    color: saveButtonShadow
+                }
+            }
+            onClicked: { virtualstudio.showWarnings = currShowWarnings; warningScreen = "acknowledged" }
+            anchors.right: parent.right
+            anchors.rightMargin: 16 * virtualstudio.uiScale
+            anchors.bottomMargin: 16 * virtualstudio.uiScale
+            anchors.bottom: parent.bottom
+            width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+            Text {
+                text: "OK"
+                font.family: "Poppins"
+                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                font.weight: Font.Bold
+                color: saveButtonText
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+            }
+        }
+
+        CheckBox {
+            id: showHeadphonesWarningCheckbox
+            checked: currShowWarnings
+            text: qsTr("Show warnings again next time")
+            anchors.right: okButtonHeadphones.left
+            anchors.rightMargin: 16 * virtualstudio.uiScale
+            anchors.verticalCenter: okButtonHeadphones.verticalCenter
+            onClicked: { currShowWarnings = showHeadphonesWarningCheckbox.checkState == Qt.Checked }
+            indicator: Rectangle {
+                implicitWidth: 16 * virtualstudio.uiScale
+                implicitHeight: 16 * virtualstudio.uiScale
+                x: showHeadphonesWarningCheckbox.leftPadding
+                y: parent.height / 2 - height / 2
+                radius: 3 * virtualstudio.uiScale
+                border.color: showHeadphonesWarningCheckbox.down ? checkboxPressedStroke : checkboxStroke
+
+                Rectangle {
+                    width: 10 * virtualstudio.uiScale
+                    height: 10 * virtualstudio.uiScale
+                    x: 3 * virtualstudio.uiScale
+                    y: 3 * virtualstudio.uiScale
+                    radius: 2 * virtualstudio.uiScale
+                    color: showHeadphonesWarningCheckbox.down ? checkboxPressedStroke : checkboxStroke
+                    visible: showHeadphonesWarningCheckbox.checked
+                }
+            }
+            contentItem: Text {
+                text: showHeadphonesWarningCheckbox.text
+                font.family: "Poppins"
+                font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+                leftPadding: showHeadphonesWarningCheckbox.indicator.width + showHeadphonesWarningCheckbox.spacing
+                color: textColour
+            }
+        }
+    }
+
+    Item {
+        id: setupItem
+        width: parent.width; height: parent.height
+        visible: warningScreen == "acknowledged"
+    
+        Text {
+            x: 16 * virtualstudio.uiScale; y: 32 * virtualstudio.uiScale
+            text: "Choose your audio devices"
+            font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+        
+        ComboBox {
+            id: backendCombo
+            model: backendComboModel
+            currentIndex: virtualstudio.audioBackend == "JACK" ? 0 : 1
+            onActivated: { virtualstudio.audioBackend = currentText }
+            x: 234 * virtualstudio.uiScale; y: 150 * virtualstudio.uiScale
+            width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale
+            visible: virtualstudio.selectableBackend
+        }
+        
+        Text {
+            id: backendLabel
+            anchors.verticalCenter: backendCombo.verticalCenter
+            x: leftMargin * virtualstudio.uiScale
+            text: "Audio Backend"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            visible: virtualstudio.selectableBackend
+            color: textColour
+        }
+        
+        Text {
+            id: jackLabel
+            x: leftMargin * virtualstudio.uiScale; y: 150 * virtualstudio.uiScale
+            width: parent.width - x - (16 * virtualstudio.uiScale)
+            text: "Using JACK for audio input and output. Use QjackCtl to adjust your sample rate, buffer, and device settings."
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            wrapMode: Text.WordWrap
+            visible: virtualstudio.audioBackend == "JACK" && !virtualstudio.selectableBackend
+            color: textColour
+        }
+        
+        ComboBox {
+            id: inputCombo
+            model: inputComboModel
+            currentIndex: virtualstudio.inputDevice
+            onActivated: { virtualstudio.inputDevice = currentIndex }
+            x: 234 * virtualstudio.uiScale; y: virtualstudio.uiScale * (virtualstudio.selectableBackend ? 198 : 150)
+            width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale
+            visible: virtualstudio.audioBackend != "JACK"
+        }
+        
+        ComboBox {
+            id: outputCombo
+            model: outputComboModel
+            currentIndex: virtualstudio.outputDevice
+            onActivated: { virtualstudio.outputDevice = currentIndex }
+            x: backendCombo.x; y: inputCombo.y + (48 * virtualstudio.uiScale)
+            width: backendCombo.width; height: backendCombo.height
+            visible: virtualstudio.audioBackend != "JACK"
+        }
+        
+        Text {
+            id: inputLabel
+            anchors.verticalCenter: inputCombo.verticalCenter
+            x: leftMargin * virtualstudio.uiScale
+            text: "Input Device"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            visible: virtualstudio.audioBackend != "JACK"
+            color: textColour
+        }
+        
+        Text {
+            id: outputLabel
+            anchors.verticalCenter: outputCombo.verticalCenter
+            x: leftMargin * virtualstudio.uiScale
+            text: "Output Device"
+            font { family: "Poppins"; pixelSize: 13 * virtualstudio.fontScale * virtualstudio.uiScale }
+            visible: virtualstudio.audioBackend != "JACK"
+            color: textColour
+        }
+
+        Button {
+            id: refreshButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: { virtualstudio.refreshDevices() }
+            x: parent.width - (232 * virtualstudio.uiScale); y: inputCombo.y + (100 * virtualstudio.uiScale)
+            width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+            visible: virtualstudio.audioBackend != "JACK"
+            Text {
+                text: "Refresh Device List"
+                font { family: "Poppins"; pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+
+        Text {
+            anchors.left: outputLabel.left
+            anchors.right: outputCombo.right
+            anchors.leftMargin: 16 * virtualstudio.uiScale
+            anchors.rightMargin: 16 * virtualstudio.uiScale
+            y: inputCombo.y + (160 * virtualstudio.uiScale)
+            text: "JackTrip on Windows requires use of an audio device with ASIO drivers. If you do not see your device, you may need to install drivers from your manufacturer."
+            horizontalAlignment: Text.AlignHCenter
+            wrapMode: Text.WordWrap
+            color: warningText
+            font { family: "Poppins"; pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale }
+            visible: Qt.platform.os == "windows" && virtualstudio.audioBackend != "JACK"
+        }
+
+        Button {
+            id: saveButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: saveButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
+                border.width: 1
+                border.color: saveButton.down ? saveButtonPressedStroke : saveButtonStroke
+                layer.enabled: saveButton.hovered && !saveButton.down
+                layer.effect: DropShadow {
+                    horizontalOffset: 1 * virtualstudio.uiScale
+                    verticalOffset: 1 * virtualstudio.uiScale
+                    radius: 8.0 * virtualstudio.uiScale
+                    samples: 17
+                    color: saveButtonShadow
+                }
+            }
+            onClicked: { window.state = "browse"; virtualstudio.applySettings() }
+            anchors.right: parent.right
+            anchors.rightMargin: 16 * virtualstudio.uiScale
+            anchors.bottomMargin: 16 * virtualstudio.uiScale
+            anchors.bottom: parent.bottom
+            width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+            Text {
+                text: "Save Settings"
+                font.family: "Poppins"
+                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                font.weight: Font.Bold
+                color: saveButtonText
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+            }
+        }
+
+        CheckBox {
+            id: showAgainCheckbox
+            checked: virtualstudio.showDeviceSetup
+            text: qsTr("Ask again next time")
+            anchors.right: saveButton.left
+            anchors.rightMargin: 16 * virtualstudio.uiScale
+            anchors.verticalCenter: saveButton.verticalCenter
+            onClicked: { virtualstudio.showDeviceSetup = showAgainCheckbox.checkState == Qt.Checked }
+            indicator: Rectangle {
+                implicitWidth: 16 * virtualstudio.uiScale
+                implicitHeight: 16 * virtualstudio.uiScale
+                x: showAgainCheckbox.leftPadding
+                y: parent.height / 2 - height / 2
+                radius: 3 * virtualstudio.uiScale
+                border.color: showAgainCheckbox.down ? checkboxPressedStroke : checkboxStroke
+
+                Rectangle {
+                    width: 10 * virtualstudio.uiScale
+                    height: 10 * virtualstudio.uiScale
+                    x: 3 * virtualstudio.uiScale
+                    y: 3 * virtualstudio.uiScale
+                    radius: 2 * virtualstudio.uiScale
+                    color: showAgainCheckbox.down ? checkboxPressedStroke : checkboxStroke
+                    visible: showAgainCheckbox.checked
+                }
+            }
+
+            contentItem: Text {
+                text: showAgainCheckbox.text
+                font.family: "Poppins"
+                font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+                leftPadding: showAgainCheckbox.indicator.width + showAgainCheckbox.spacing
+                color: textColour
+            }
+        }
+    }
+}
diff --git a/src/gui/Studio.qml b/src/gui/Studio.qml
new file mode 100644 (file)
index 0000000..f2004f5
--- /dev/null
@@ -0,0 +1,210 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtGraphicalEffects 1.12
+
+Rectangle {
+    width: 664; height: 83 * virtualstudio.uiScale
+    radius: 6 * virtualstudio.uiScale
+    color: backgroundColour
+    border.width: 0.3
+    border.color: "#40979797"
+
+    layer.enabled: true
+    layer.effect: DropShadow {
+        horizontalOffset: 1 * virtualstudio.uiScale
+        verticalOffset: 1 * virtualstudio.uiScale
+        radius: 8.0 * virtualstudio.uiScale
+        samples: 17
+        color: shadowColour
+    }
+    
+    property string serverLocation: "Germany - Berlin"
+    property string flagImage: "flags/DE.svg"
+    property string studioName: "Test Studio"
+    property bool publicStudio: false
+    property bool manageable: false
+    property bool available: true
+    property bool connected: false
+    
+    property int leftMargin: 81
+    property int topMargin: 13
+    
+    property real fontBig: 18
+    property real fontMedium: 11
+    property real fontSmall: 8
+    
+    property string backgroundColour: virtualstudio.darkMode ? "#494646" : "#F4F6F6"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1"
+    property string joinColour: virtualstudio.darkMode ? (connected ? "#FCB6B6" : "#E2EBE0") : (connected ? "#FCB6B6" : "#C4F4BE")
+    property string joinHoverColour: virtualstudio.darkMode ? (connected ? "#D49696" : "#BAC7B8") : (connected ? "#E3A4A4" : "#B0DCAB")
+    property string joinPressedColour: virtualstudio.darkMode ? (connected ? "#F2AEAE" : "#D8E2D6") : (connected ? "#EFADAD" : "#BAE8B5")
+    property string joinStroke: virtualstudio.darkMode ? (connected ? "#A65959" : "#748F70") : (connected ? "#C95E5E" : "#5DB752")
+    property string manageColour: virtualstudio.darkMode ? "#F0F1F1" : "#EAEBEB"
+    property string manageHoverColour: virtualstudio.darkMode ? "#CCCDCD" : "#D3D3D3"
+    property string managePressedColour: virtualstudio.darkMode ? "#E4E5E5" : "#EAEBEB"
+    property string manageStroke: virtualstudio.darkMode ? "#8B8D8D" : "#949494"
+
+    Rectangle {
+        id: shadow
+        anchors.fill: parent
+        color: "transparent"
+        radius: 6
+    }
+
+    DropShadow {
+        horizontalOffset: -1 * virtualstudio.uiScale
+        verticalOffset: -1 * virtualstudio.uiScale
+        radius: 8.0 * virtualstudio.uiScale
+        samples: 17
+        color: shadowColour
+        source: shadow
+    }
+    
+    Rectangle {
+        width: 12 * virtualstudio.uiScale; height: parent.height
+        radius: width / 2
+        color: available ? "#0C1424" : "#B3B3B3"
+    }
+    
+    Image {
+        source: available ? "wedge.svg" : "wedge_inactive.svg"
+        x: 6; y: 0; width: 52 * virtualstudio.uiScale; height: 83 * virtualstudio.uiScale
+    }
+    
+    Image {
+        source: "logo.svg"
+        x: 8; y: 11; width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale
+    }
+    
+    Rectangle {
+        x: 33 * virtualstudio.uiScale; y: 8 * virtualstudio.uiScale
+        width: 32 * virtualstudio.uiScale; height: width
+        radius: width / 2
+        color: available ? "#0C1424" : "#B3B3B3"
+    }
+    
+    Image {
+        id: flag
+        source: flagImage
+        x: 30 * virtualstudio.uiScale; y: 9 * virtualstudio.uiScale
+        width: 40 * virtualstudio.uiScale; height: width / 4 * 3
+        fillMode: Image.PreserveAspectCrop
+        layer.enabled: true
+        layer.effect: OpacityMask {
+            maskSource: mask
+        }
+    }
+
+    Rectangle {
+        id: mask
+        x: 0 ; y: 0 ; width: flag.width; height: flag.height
+        visible: false
+        color: "#00000000"
+        Rectangle {
+            x: 7 * virtualstudio.uiScale; y: 3 * virtualstudio.uiScale
+            width:24 * virtualstudio.uiScale; height: width
+            radius: width / 2
+        }
+    }
+    
+    Text {
+        x: leftMargin * virtualstudio.uiScale; y: 11 * virtualstudio.uiScale;
+        width: manageable ? parent.width - (233 * virtualstudio.uiScale) : parent.width - (156 * virtualstudio.uiScale)
+        text: studioName
+        font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
+        elide: Text.ElideRight
+        color: textColour
+    }
+    
+    Rectangle {
+        id: publicRect
+        x: leftMargin * virtualstudio.uiScale; y: 52 * virtualstudio.uiScale
+        width: 14 * virtualstudio.uiScale; height: width
+        radius: 2 * virtualstudio.uiScale
+        color: publicStudio ? "#0095FF" : "#FF9800"
+        Image {
+            source: publicStudio ? "public.svg" : "private.svg"
+            x: 1 * virtualstudio.uiScale; y: x; width: 12 * virtualstudio.uiScale; height: width
+        }
+    }
+    
+    Text {
+        anchors.verticalCenter: publicRect.verticalCenter
+        x: (leftMargin + 22) * virtualstudio.uiScale
+        width: manageable ? parent.width - (255 * virtualstudio.uiScale) : parent.width - (178 * virtualstudio.uiScale)
+        text: publicStudio ? "Public hub studio in " + serverLocation : "Private hub studio in " + serverLocation
+        font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+        elide: Text.ElideRight
+        color: textColour
+    }
+    
+    Button {
+        id: joinButton
+        x: manageable ? parent.width - (142 * virtualstudio.uiScale) : parent.width - (65 * virtualstudio.uiScale)
+        y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width
+        background: Rectangle {
+            radius: width / 2
+            color: joinButton.down ? joinPressedColour : (joinButton.hovered ? joinHoverColour : joinColour)
+            border.width: joinButton.down ? 1 : 0
+            border.color: joinStroke
+        }
+        visible: connected || canConnect || canStart
+        onClicked: {
+            if (!connected) {
+                window.state = "connected";
+                virtualstudio.connectToStudio(index);
+            } else {
+                virtualstudio.disconnect();
+            }
+        }
+        Image {
+            width: 22 * virtualstudio.uiScale; height: 20 * virtualstudio.uiScale
+            anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+            source: connected ? "leave.svg" : "join.svg"
+        }
+    }
+    
+    Text {
+        anchors.horizontalCenter: joinButton.horizontalCenter
+        y: 56 * virtualstudio.uiScale
+        text: connected ? "Leave" : available ? "Join" : "Start"
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
+        visible: connected || canConnect || canStart
+        color: textColour
+    }
+    
+    Button {
+        id: manageButton
+        x: parent.width - (65 * virtualstudio.uiScale); y: topMargin * virtualstudio.uiScale
+        width: 40 * virtualstudio.uiScale; height: width
+        background: Rectangle {
+            radius: width / 2
+            color: manageButton.down ? managePressedColour : (manageButton.hovered ? manageHoverColour : manageColour)
+            border.width:  manageButton.down ? 1 : 0
+            border.color: manageStroke
+        }
+        onClicked: { 
+            if (!connected) {
+                virtualstudio.manageStudio(index)
+            } else {
+                virtualstudio.manageStudio(-1)
+            }
+        }
+        visible: manageable
+        Image {
+            width: 20 * virtualstudio.uiScale; height: width
+            anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+            source: "cog.svg"
+        }
+    }
+    
+    Text {
+        anchors.horizontalCenter: manageButton.horizontalCenter
+        y: 56 * virtualstudio.uiScale
+        text: "Manage"
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        visible: manageable
+        color: textColour
+    }
+}
diff --git a/src/gui/cog.svg b/src/gui/cog.svg
new file mode 100644 (file)
index 0000000..ba5645a
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 13.5C9.07177 13.5 8.18153 13.1313 7.52515 12.4749C6.86877 11.8185 6.50002 10.9283 6.50002 10C6.50002 9.07174 6.86877 8.1815 7.52515 7.52513C8.18153 6.86875 9.07177 6.5 10 6.5C10.9283 6.5 11.8185 6.86875 12.4749 7.52513C13.1313 8.1815 13.5 9.07174 13.5 10C13.5 10.9283 13.1313 11.8185 12.4749 12.4749C11.8185 13.1313 10.9283 13.5 10 13.5V13.5ZM17.43 10.97C17.47 10.65 17.5 10.33 17.5 10C17.5 9.67 17.47 9.34 17.43 9L19.54 7.37C19.73 7.22 19.78 6.95 19.66 6.73L17.66 3.27C17.54 3.05 17.27 2.96 17.05 3.05L14.56 4.05C14.04 3.66 13.5 3.32 12.87 3.07L12.5 0.42C12.46 0.18 12.25 0 12 0H8.00002C7.75002 0 7.54002 0.18 7.50002 0.42L7.13002 3.07C6.50002 3.32 5.96002 3.66 5.44002 4.05L2.95002 3.05C2.73002 2.96 2.46002 3.05 2.34002 3.27L0.340022 6.73C0.210022 6.95 0.270023 7.22 0.460023 7.37L2.57002 9C2.53002 9.34 2.50002 9.67 2.50002 10C2.50002 10.33 2.53002 10.65 2.57002 10.97L0.460023 12.63C0.270023 12.78 0.210022 13.05 0.340022 13.27L2.34002 16.73C2.46002 16.95 2.73002 17.03 2.95002 16.95L5.44002 15.94C5.96002 16.34 6.50002 16.68 7.13002 16.93L7.50002 19.58C7.54002 19.82 7.75002 20 8.00002 20H12C12.25 20 12.46 19.82 12.5 19.58L12.87 16.93C13.5 16.67 14.04 16.34 14.56 15.94L17.05 16.95C17.27 17.03 17.54 16.95 17.66 16.73L19.66 13.27C19.78 13.05 19.73 12.78 19.54 12.63L17.43 10.97Z" fill="#494646"/>
+</svg>
diff --git a/src/gui/ethernet.png b/src/gui/ethernet.png
new file mode 100644 (file)
index 0000000..c911dc4
Binary files /dev/null and b/src/gui/ethernet.png differ
diff --git a/src/gui/flags/AE.svg b/src/gui/flags/AE.svg
new file mode 100644 (file)
index 0000000..59ddafd
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><path fill="#009e49" d="M0 0h513v114H0z"/><path d="M0 228h513v114H0z"/><path fill="#ce1126" d="M0 0h171v342H0z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/AU.svg b/src/gui/flags/AU.svg
new file mode 100644 (file)
index 0000000..f91b013
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#10338c" d="M0 0h513v342H0z"/><g fill="#FFF"><path d="M222.2 170.7c.3-.3.5-.6.8-.9-.2.3-.5.6-.8.9zM188 212.6l11 22.9 24.7-5.7-11 22.8 19.9 15.8-24.8 5.6.1 25.4-19.9-15.9-19.8 15.9.1-25.4-24.8-5.6 19.9-15.8-11.1-22.8 24.8 5.7zM385.9 241.1l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.6v12.2l-9.4-7.6-9.5 7.6.1-12.2-11.8-2.6 9.5-7.5-5.3-10.9 11.8 2.7zM337.3 125.1l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.7v12.1l-9.4-7.6-9.5 7.6.1-12.1-11.9-2.7 9.5-7.5-5.3-10.9L332 136zM385.9 58.9l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.7v12.1l-9.4-7.6-9.5 7.6.1-12.1-11.8-2.7 9.5-7.5-5.3-10.9 11.8 2.7zM428.4 108.6l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.6V150l-9.4-7.6-9.5 7.6v-12.2l-11.8-2.6 9.5-7.5-5.3-10.9 11.8 2.7zM398 166.5l4.1 12.7h13.3l-10.8 7.8 4.2 12.7-10.8-7.9-10.8 7.9 4.1-12.7-10.7-7.8h13.3z"/><path d="M254.8 0v30.6l-45.1 25.1h45.1V115h-59.1l59.1 32.8v22.9h-26.7l-73.5-40.9v40.9H99v-48.6l-87.4 48.6H-1.2v-30.6L44 115H-1.2V55.7h59.1L-1.2 22.8V0h26.7L99 40.8V0h55.6v48.6L242.1 0z"/></g><path fill="#D80027" d="M142.8 0h-32v69.3h-112v32h112v69.4h32v-69.4h112v-32h-112z"/><path fill="#0052B4" d="m154.6 115 100.2 55.7v-15.8L183 115z"/><path fill="#FFF" d="m154.6 115 100.2 55.7v-15.8L183 115z"/><g fill="#D80027"><path d="m154.6 115 100.2 55.7v-15.8L183 115zM70.7 115l-71.9 39.9v15.8L99 115z"/></g><path fill="#0052B4" d="M99 55.7-1.2 0v15.7l71.9 40z"/><path fill="#FFF" d="M99 55.7-1.2 0v15.7l71.9 40z"/><g fill="#D80027"><path d="M99 55.7-1.2 0v15.7l71.9 40zM183 55.7l71.8-40V0L154.6 55.7z"/></g></svg>
\ No newline at end of file
diff --git a/src/gui/flags/BE.svg b/src/gui/flags/BE.svg
new file mode 100644 (file)
index 0000000..cc1b013
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#fdda25" d="M0 0h513v342H0z"/><path d="M0 0h171v342H0z"/><path fill="#ef3340" d="M342 0h171v342H342z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/BR.svg b/src/gui/flags/BR.svg
new file mode 100644 (file)
index 0000000..f4dbb02
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#009b3a" d="M0 0h513v342H0z"/><path fill="#fedf00" d="m256.5 19.3 204.9 151.4L256.5 322 50.6 170.7z"/><circle fill="#FFF" cx="256.5" cy="171" r="80.4"/><path fill="#002776" d="M215.9 165.7c-13.9 0-27.4 2.1-40.1 6 .6 43.9 36.3 79.3 80.3 79.3 27.2 0 51.3-13.6 65.8-34.3-24.9-31-63.2-51-106-51zM334.9 186c.9-5 1.5-10.1 1.5-15.4 0-44.4-36-80.4-80.4-80.4-33.1 0-61.5 20.1-73.9 48.6 10.9-2.2 22.1-3.4 33.6-3.4 46.8.1 89 19.5 119.2 50.6z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/CA.svg b/src/gui/flags/CA.svg
new file mode 100644 (file)
index 0000000..457d316
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><g fill="red"><path d="M0 0h142v342H0zM371 0h142v342H371zM306.5 206l50.4-25.2-25.2-12.6V143l-50.4 25.2 25.2-50.4h-25.2L256.1 80l-25.2 37.8h-25.2l25.2 50.4-50.4-25.2v25.2l-25.2 12.6 50.4 25.2-12.6 25.2h50.4V269h25.2v-37.8h50.4z"/></g></svg>
\ No newline at end of file
diff --git a/src/gui/flags/CH.svg b/src/gui/flags/CH.svg
new file mode 100644 (file)
index 0000000..498b7d1
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 513 342"><path fill="red" d="M0 85.337h513v342H0z"/><path fill="#FFF" d="M356.174 222.609h-66.783v-66.783h-66.782v66.783h-66.783v66.782h66.783v66.783h66.782v-66.783h66.783z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/DE.svg b/src/gui/flags/DE.svg
new file mode 100644 (file)
index 0000000..df0775b
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.331h512v341.337H0z"/><path d="M0 85.331h512v113.775H0z"/><path fill="#FFDA44" d="M0 312.882h512v113.775H0z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/FR.svg b/src/gui/flags/FR.svg
new file mode 100644 (file)
index 0000000..9f02836
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.331h512v341.337H0z"/><path fill="#0052B4" d="M0 85.331h170.663v341.337H0z"/><path fill="#D80027" d="M341.337 85.331H512v341.337H341.337z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/GB.svg b/src/gui/flags/GB.svg
new file mode 100644 (file)
index 0000000..4ada58a
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.333h512V426.67H0z"/><path fill="#D80027" d="M288 85.33h-64v138.666H0v64h224v138.666h64V287.996h224v-64H288z"/><g fill="#0052B4"><path d="M393.785 315.358 512 381.034v-65.676zM311.652 315.358 512 426.662v-31.474l-143.693-79.83zM458.634 426.662l-146.982-81.664v81.664z"/></g><path fill="#FFF" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><path fill="#D80027" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><g fill="#0052B4"><path d="M90.341 315.356 0 365.546v-50.19zM200.348 329.51v97.151H25.491z"/></g><path fill="#D80027" d="M143.693 315.358 0 395.188v31.474l200.348-111.304z"/><g fill="#0052B4"><path d="M118.215 196.634 0 130.958v65.676zM200.348 196.634 0 85.33v31.474l143.693 79.83zM53.366 85.33l146.982 81.664V85.33z"/></g><path fill="#FFF" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><path fill="#D80027" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><g fill="#0052B4"><path d="M421.659 196.636 512 146.446v50.19zM311.652 182.482V85.331h174.857z"/></g><path fill="#D80027" d="M368.307 196.634 512 116.804V85.33L311.652 196.634z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/HK.svg b/src/gui/flags/HK.svg
new file mode 100644 (file)
index 0000000..284a722
--- /dev/null
@@ -0,0 +1 @@
+<svg viewBox="0 0.5 21 14" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FFF" d="M0 0h21v15H0z"/><path fill="#ee1c25" d="M0 0h21v15H0z"/><path d="M12 7.19c-.798-.5-1 .409-1 0 0-.828.895-1.5 2-1.5s2 .672 2 1.5c-.949 0-1.044.5-1.5.5-.56 0-.702 0-1.5-.5zM13.25 7a.25.25 0 1 0 0-.5.25.25 0 0 0 0 .5zm-1.81 1.962c.228-.913-.698-.824-.31-.95.788-.257 1.703.387 2.045 1.438.341 1.05-.021 2.11-.809 2.366-.293-.903-.798-.838-.939-1.272-.173-.533-.217-.668.012-1.582zm.566 1.13a.25.25 0 1 0 .476-.154.25.25 0 0 0-.476.154zM9.58 8.977c.94-.065.57-.919.81-.588.486.67.157 1.74-.737 2.389-.894.65-2.013.632-2.5-.038.768-.558.55-1.018.92-1.286.453-.33.568-.413 1.507-.477zm-.899.888a.25.25 0 1 0 .294.405.25.25 0 0 0-.294-.405zm.312-2.652c.351.874 1.049.258.809.588-.487.67-1.606.687-2.5.038-.894-.65-1.223-1.719-.736-2.39.767.559 1.138.21 1.507.478.453.33.568.413.92 1.286zm-1.124-.58a.25.25 0 1 0-.293.404.25.25 0 0 0 .293-.404zm2.619-.524c-.722.605.08 1.078-.309.951-.788-.256-1.15-1.315-.809-2.365.342-1.05 1.257-1.695 2.045-1.439-.293.903.153 1.147.012 1.581-.173.533-.217.668-.939 1.272zm.205-1.247a.25.25 0 1 0-.475-.155.25.25 0 0 0 .475.155z" fill="#FFF"/></g></svg>
\ No newline at end of file
diff --git a/src/gui/flags/ID.svg b/src/gui/flags/ID.svg
new file mode 100644 (file)
index 0000000..45d3745
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.333h512v341.333H0z"/><path fill="#E00" d="M0 85.333h512V256H0z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/IT.svg b/src/gui/flags/IT.svg
new file mode 100644 (file)
index 0000000..17b1314
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M341.334 85.33H0v341.332h512V85.33z"/><path fill="#6DA544" d="M0 85.333h170.663V426.67H0z"/><path fill="#D80027" d="M341.337 85.333H512V426.67H341.337z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/JP.svg b/src/gui/flags/JP.svg
new file mode 100644 (file)
index 0000000..92eb885
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.331h512v341.337H0z"/><circle fill="#D80027" cx="256" cy="255.994" r="96"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/RO.svg b/src/gui/flags/RO.svg
new file mode 100644 (file)
index 0000000..fabf12e
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFDA44" d="M0 85.331h512v341.326H0z"/><path fill="#0052B4" d="M0 85.331h170.663v341.337H0z"/><path fill="#D80027" d="M341.337 85.331H512v341.337H341.337z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/SE.svg b/src/gui/flags/SE.svg
new file mode 100644 (file)
index 0000000..7ec1787
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#0052B4" d="M0 85.333h512V426.67H0z"/><path fill="#FFDA44" d="M192 85.33h-64v138.666H0v64h128v138.666h64V287.996h320v-64H192z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/SG.svg b/src/gui/flags/SG.svg
new file mode 100644 (file)
index 0000000..c374c47
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.337h512v341.326H0z"/><path fill="#D80027" d="M0 85.337h512V256H0z"/><g fill="#FFF"><path d="M83.478 170.666c0-24.865 17.476-45.637 40.812-50.734a52.059 52.059 0 0 0-11.13-1.208c-28.688 0-51.942 23.254-51.942 51.941s23.255 51.942 51.942 51.942c3.822 0 7.543-.425 11.13-1.208-23.336-5.095-40.812-25.867-40.812-50.733zM150.261 122.435l3.684 11.337h11.921l-9.645 7.007 3.684 11.337-9.644-7.006-9.645 7.006 3.685-11.337-9.645-7.007h11.921z"/><path d="m121.344 144.696 3.683 11.337h11.921l-9.645 7.007 3.684 11.337-9.643-7.006-9.645 7.006 3.685-11.337-9.645-7.007h11.921zM179.178 144.696l3.684 11.337h11.921l-9.645 7.007 3.684 11.337-9.644-7.006-9.644 7.006 3.685-11.337-9.645-7.007h11.921zM168.047 178.087l3.684 11.337h11.921l-9.644 7.007 3.684 11.337-9.645-7.006-9.643 7.006 3.684-11.337-9.644-7.007h11.92zM132.474 178.087l3.683 11.337h11.921l-9.644 7.007 3.684 11.337-9.644-7.006-9.644 7.006 3.684-11.337-9.644-7.007h11.92z"/></g></svg>
\ No newline at end of file
diff --git a/src/gui/flags/TW.svg b/src/gui/flags/TW.svg
new file mode 100644 (file)
index 0000000..c3660f1
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.337h512v341.326H0z"/><path fill="#0052B4" d="M0 85.337h256V256H0z"/><path fill="#FFF" d="M186.435 170.669 162.558 181.9l12.714 23.125-25.927-4.961-3.286 26.192L128 206.993l-18.06 19.263-3.285-26.192-25.927 4.96 12.714-23.125-23.877-11.23 23.877-11.231-12.714-23.125 25.927 4.96 3.286-26.192L128 134.344l18.06-19.263 3.285 26.192 25.928-4.96-12.715 23.125z"/><circle fill="#0052B4" cx="128" cy="170.674" r="29.006"/><path fill="#FFF" d="M128 190.06c-10.692 0-19.391-8.7-19.391-19.391 0-10.692 8.7-19.391 19.391-19.391 10.692 0 19.391 8.7 19.391 19.391 0 10.691-8.699 19.391-19.391 19.391z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/US.svg b/src/gui/flags/US.svg
new file mode 100644 (file)
index 0000000..dc427e7
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><g fill="#D80027"><path d="M0 0h513v26.3H0zM0 52.6h513v26.3H0zM0 105.2h513v26.3H0zM0 157.8h513v26.3H0zM0 210.5h513v26.3H0zM0 263.1h513v26.3H0zM0 315.7h513V342H0z"/></g><path fill="#2E52B2" d="M0 0h256.5v184.1H0z"/><g fill="#FFF"><path d="m47.8 138.9-4-12.8-4.4 12.8H26.2l10.7 7.7-4 12.8 10.9-7.9 10.6 7.9-4.1-12.8 10.9-7.7zM104.1 138.9l-4.1-12.8-4.2 12.8H82.6l10.7 7.7-4 12.8 10.7-7.9 10.8 7.9-4-12.8 10.7-7.7zM160.6 138.9l-4.3-12.8-4 12.8h-13.5l11 7.7-4.2 12.8 10.7-7.9 11 7.9-4.2-12.8 10.7-7.7zM216.8 138.9l-4-12.8-4.2 12.8h-13.3l10.8 7.7-4 12.8 10.7-7.9 10.8 7.9-4.3-12.8 11-7.7zM100 75.3l-4.2 12.8H82.6L93.3 96l-4 12.6 10.7-7.8 10.8 7.8-4-12.6 10.7-7.9h-13.4zM43.8 75.3l-4.4 12.8H26.2L36.9 96l-4 12.6 10.9-7.8 10.6 7.8L50.3 96l10.9-7.9H47.8zM156.3 75.3l-4 12.8h-13.5l11 7.9-4.2 12.6 10.7-7.8 11 7.8-4.2-12.6 10.7-7.9h-13.2zM212.8 75.3l-4.2 12.8h-13.3l10.8 7.9-4 12.6 10.7-7.8 10.8 7.8-4.3-12.6 11-7.9h-13.5zM43.8 24.7l-4.4 12.6H26.2l10.7 7.9-4 12.7L43.8 50l10.6 7.9-4.1-12.7 10.9-7.9H47.8zM100 24.7l-4.2 12.6H82.6l10.7 7.9-4 12.7L100 50l10.8 7.9-4-12.7 10.7-7.9h-13.4zM156.3 24.7l-4 12.6h-13.5l11 7.9-4.2 12.7 10.7-7.9 11 7.9-4.2-12.7 10.7-7.9h-13.2zM212.8 24.7l-4.2 12.6h-13.3l10.8 7.9-4 12.7 10.7-7.9 10.8 7.9-4.3-12.7 11-7.9h-13.5z"/></g></svg>
\ No newline at end of file
diff --git a/src/gui/flags/ZA.svg b/src/gui/flags/ZA.svg
new file mode 100644 (file)
index 0000000..1b294c9
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.337h512v341.326H0z"/><path d="M114.024 256.001 0 141.926v228.17z"/><path fill="#ffb915" d="M161.192 256 0 94.7v47.226l114.024 114.075L0 370.096v47.138z"/><path fill="#007847" d="M509.833 289.391c.058-.44.804-.878 2.167-1.318v-65.464H222.602L85.33 85.337H0V94.7L161.192 256 0 417.234v9.429h85.33l137.272-137.272h287.231z"/><path fill="#000c8a" d="M503.181 322.783H236.433l-103.881 103.88H512v-103.88z"/><path fill="#e1392d" d="M503.181 189.217H512V85.337H132.552l103.881 103.88z"/></svg>
\ No newline at end of file
diff --git a/src/gui/headphones.svg b/src/gui/headphones.svg
new file mode 100644 (file)
index 0000000..fa3a213
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="24" height="26" viewBox="0 0 24 26" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12 0C8.81846 0.00344108 5.76821 1.26883 3.51852 3.51852C1.26883 5.76821 0.00344108 8.81846 0 12V22.444C0.00105872 23.3868 0.376048 24.2907 1.0427 24.9573C1.70935 25.624 2.61322 25.9989 3.556 26H6C6.26522 26 6.51957 25.8946 6.70711 25.7071C6.89464 25.5196 7 25.2652 7 25V15C7 14.7348 6.89464 14.4804 6.70711 14.2929C6.51957 14.1054 6.26522 14 6 14H2V12C2 9.34784 3.05357 6.8043 4.92893 4.92893C6.8043 3.05357 9.34784 2 12 2C14.6522 2 17.1957 3.05357 19.0711 4.92893C20.9464 6.8043 22 9.34784 22 12V14H18C17.7348 14 17.4804 14.1054 17.2929 14.2929C17.1054 14.4804 17 14.7348 17 15V25C17 25.2652 17.1054 25.5196 17.2929 25.7071C17.4804 25.8946 17.7348 26 18 26H20.444C21.3868 25.9989 22.2907 25.624 22.9573 24.9573C23.624 24.2907 23.9989 23.3868 24 22.444V12C23.9966 8.81846 22.7312 5.76821 20.4815 3.51852C18.2318 1.26883 15.1815 0.00344108 12 0Z" fill="#0F0D0D"/>
+</svg>
diff --git a/src/gui/jacktrip white.png b/src/gui/jacktrip white.png
new file mode 100644 (file)
index 0000000..7ba69b0
Binary files /dev/null and b/src/gui/jacktrip white.png differ
diff --git a/src/gui/jacktrip.png b/src/gui/jacktrip.png
new file mode 100644 (file)
index 0000000..c4b998a
Binary files /dev/null and b/src/gui/jacktrip.png differ
diff --git a/src/gui/join.svg b/src/gui/join.svg
new file mode 100644 (file)
index 0000000..a52a534
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="23" height="19" viewBox="0 0 23 19" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 3C9.06087 3 10.0783 3.42143 10.8284 4.17157C11.5786 4.92172 12 5.93913 12 7C12 8.06087 11.5786 9.07828 10.8284 9.82843C10.0783 10.5786 9.06087 11 8 11C6.93913 11 5.92172 10.5786 5.17157 9.82843C4.42143 9.07828 4 8.06087 4 7C4 5.93913 4.42143 4.92172 5.17157 4.17157C5.92172 3.42143 6.93913 3 8 3V3ZM8 13C10.67 13 16 14.34 16 17V19H0V17C0 14.34 5.33 13 8 13ZM15.76 3.36C17.78 5.56 17.78 8.61 15.76 10.63L14.08 8.94C14.92 7.76 14.92 6.23 14.08 5.05L15.76 3.36ZM19.07 0C23 4.05 22.97 10.11 19.07 14L17.44 12.37C20.21 9.19 20.21 4.65 17.44 1.63L19.07 0Z" fill="#204C1A"/>
+</svg>
diff --git a/src/gui/leave.svg b/src/gui/leave.svg
new file mode 100644 (file)
index 0000000..c44f7e7
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1 1.27L2.28 0L21 18.72L19.73 20L15.73 16C15.9 16.31 16 16.64 16 17V19H0V17C0 14.34 5.33 13 8 13C9.77 13 12.72 13.59 14.5 14.77L10.12 10.39C9.5 10.78 8.78 11 8 11C6.93913 11 5.92172 10.5786 5.17157 9.82843C4.42143 9.07828 4 8.06087 4 7C4 6.22 4.22 5.5 4.61 4.88L1 1.27ZM8 3C9.06087 3 10.0783 3.42143 10.8284 4.17157C11.5786 4.92172 12 5.93913 12 7V7.17L7.83 3H8ZM15.76 3.36C17.78 5.56 17.78 8.61 15.76 10.63L14.08 8.94C14.92 7.76 14.92 6.23 14.08 5.05L15.76 3.36ZM19.07 0C23 4.05 22.97 10.11 19.07 14L17.44 12.37C20.21 9.19 20.21 4.65 17.44 1.63L19.07 0Z" fill="#9C0707"/>
+</svg>
diff --git a/src/gui/logo.svg b/src/gui/logo.svg
new file mode 100644 (file)
index 0000000..508c81b
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="50" height="93" viewBox="0 0 50 93" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.7628 73.8622C11.7925 73.9104 11.8196 73.9602 11.8441 74.0113L14.6714 78.9066C16.8218 82.5868 20.3392 85.2682 24.4576 86.3667C28.576 87.4652 32.9617 86.8917 36.6592 84.7713L36.7948 84.6967C40.4339 82.5266 43.0758 79.0148 44.1522 74.9168C45.2287 70.8188 44.6538 66.462 42.5511 62.7835L24.55 31.5952C23.4582 31.5595 22.4078 31.169 21.5579 30.4828C20.708 29.7966 20.1049 28.8521 19.84 27.7924C19.5751 26.7327 19.6627 25.6155 20.0897 24.61C20.5166 23.6046 21.2597 22.7657 22.2062 22.2204C23.1527 21.6752 24.2511 21.4533 25.3351 21.5883C26.419 21.7233 27.4295 22.208 28.2133 22.9688C28.9971 23.7296 29.5116 24.7251 29.6788 25.8046C29.8461 26.8841 29.6569 27.9886 29.1401 28.9509L47.148 60.1393C49.9521 65.0155 50.726 70.799 49.3027 76.2409C47.8795 81.6829 44.3731 86.3468 39.5407 89.2258C39.4104 89.3213 39.272 89.4052 39.1272 89.4767C34.2442 92.2178 28.4827 92.9402 23.0741 91.4895C17.6655 90.0389 13.0389 86.5302 10.183 81.7135C10.0968 81.587 10.0219 81.4531 9.95926 81.3135L7.24723 76.6284L7.16587 76.4996C6.84576 75.9775 6.33816 75.5975 5.747 75.4374C5.15584 75.2773 4.52584 75.3493 3.98601 75.6386C3.68462 75.8135 3.35172 75.9274 3.00632 75.9737C2.66092 76.02 2.30977 75.9978 1.97294 75.9084C1.63611 75.819 1.32019 75.6641 1.04321 75.4526C0.766237 75.2411 0.533626 74.9772 0.358668 74.6758C0.18371 74.3744 0.0698312 74.0415 0.0235283 73.6961C-0.0227746 73.3507 -0.000592648 72.9995 0.0888089 72.6627C0.17821 72.3259 0.333083 72.01 0.544578 71.733C0.756074 71.456 1.02005 71.2234 1.32144 71.0484C3.07661 70.0427 5.15809 69.7712 7.11251 70.2931C9.06693 70.8151 10.7359 72.0881 11.756 73.835L11.7628 73.8622ZM30.747 8.18523e-07C29.4452 -0.000742291 28.194 0.504523 27.2579 1.40909C26.3217 2.31366 25.7737 3.54669 25.7297 4.84775L15.6816 10.6515C15.6305 10.676 15.5808 10.7031 15.5325 10.7329C11.7676 12.916 9.0218 16.5028 7.89703 20.707C6.77225 24.9113 7.36024 29.3899 9.53211 33.1614L9.62703 33.3241L9.7084 33.4597L30.6249 69.6924C30.9206 70.2333 30.9936 70.8682 30.8283 71.462C30.6738 72.056 30.2913 72.5652 29.7639 72.8791C29.5615 72.992 29.3766 73.1336 29.2147 73.2994V73.2994C28.7619 73.7725 28.4992 74.3958 28.4767 75.0503C28.4542 75.7047 28.6734 76.3446 29.0927 76.8476C29.5119 77.3507 30.1017 77.6818 30.7495 77.7777C31.3973 77.8736 32.0577 77.7275 32.6047 77.3675C34.2343 76.341 35.4179 74.739 35.9204 72.8798C36.4229 71.0207 36.2074 69.0405 35.3168 67.333C35.2451 67.1605 35.1541 66.9968 35.0456 66.8448L14.2511 30.829L14.1697 30.6799L14.0748 30.5104C12.6177 27.9552 12.2276 24.9282 12.9893 22.0871C13.751 19.246 15.6029 16.8202 18.1428 15.3365L18.2784 15.2552L28.3197 9.45822C28.992 9.81663 29.7371 10.0172 30.4985 10.0448C31.2599 10.0723 32.0176 9.92613 32.714 9.61725C33.4105 9.30837 34.0275 8.84494 34.5182 8.2621C35.0088 7.67927 35.3604 6.99235 35.546 6.25343C35.7317 5.51451 35.7466 4.743 35.5897 3.99745C35.4328 3.2519 35.1081 2.55189 34.6404 1.95049C34.1726 1.3491 33.574 0.862131 32.8901 0.526529C32.2061 0.190926 31.4546 0.0154886 30.6927 0.0135535L30.747 8.18523e-07Z" fill="#F21B1B"/>
+</svg>
diff --git a/src/gui/mic.svg b/src/gui/mic.svg
new file mode 100644 (file)
index 0000000..2568def
--- /dev/null
@@ -0,0 +1,4 @@
+<svg width="19" height="28" viewBox="0 0 19 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.7516 5.43539C14.7516 2.43351 12.3181 0 9.31624 0C6.31436 0 3.88086 2.43351 3.88086 5.43539V13.4077C3.88086 16.4096 6.31436 18.8431 9.31624 18.8431C12.3181 18.8431 14.7516 16.4096 14.7516 13.4077V5.43539Z" fill="#0F0D0D"/>
+<path d="M0.906172 12.2445C0.66584 12.2445 0.435352 12.34 0.265412 12.5099C0.0954714 12.6799 0 12.9104 0 13.1507C0 17.9818 3.69688 21.9658 8.41039 22.4224V26.1882H4.7868C4.66596 26.1852 4.54575 26.2065 4.43324 26.2507C4.32074 26.2949 4.21821 26.3611 4.1317 26.4455C4.04518 26.5299 3.97643 26.6308 3.92949 26.7422C3.88255 26.8536 3.85837 26.9732 3.85837 27.0941C3.85837 27.215 3.88255 27.3346 3.92949 27.446C3.97643 27.5574 4.04518 27.6583 4.1317 27.7427C4.21821 27.8271 4.32074 27.8934 4.43324 27.9375C4.54575 27.9817 4.66596 28.003 4.7868 28H13.8463C14.0827 27.9942 14.3075 27.8962 14.4727 27.7269C14.6378 27.5577 14.7302 27.3306 14.7302 27.0941C14.7302 26.8576 14.6378 26.6305 14.4727 26.4613C14.3075 26.292 14.0827 26.194 13.8463 26.1882H10.2227V22.4219C14.9363 21.9647 18.6331 17.9813 18.6331 13.1502C18.6273 12.9138 18.5293 12.689 18.3601 12.5238C18.1908 12.3587 17.9637 12.2663 17.7272 12.2663C17.4908 12.2663 17.2636 12.3587 17.0944 12.5238C16.9251 12.689 16.8271 12.9138 16.8213 13.1502C16.8213 17.2878 13.4548 20.6544 9.31711 20.6544C5.17945 20.6544 1.81234 17.2884 1.81234 13.1507C1.81234 12.9104 1.71687 12.6799 1.54693 12.5099C1.37699 12.34 1.1465 12.2445 0.906172 12.2445Z" fill="#0F0D0D"/>
+</svg>
diff --git a/src/gui/private.svg b/src/gui/private.svg
new file mode 100644 (file)
index 0000000..c08eb3d
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9 4H8.5V3C8.5 1.62 7.38 0.5 6 0.5C4.62 0.5 3.5 1.62 3.5 3V4H3C2.45 4 2 4.45 2 5V10C2 10.55 2.45 11 3 11H9C9.55 11 10 10.55 10 10V5C10 4.45 9.55 4 9 4ZM4.45 3C4.45 2.145 5.145 1.45 6 1.45C6.855 1.45 7.55 2.145 7.55 3V4H4.45V3ZM8 8H6.5V9.5H5.5V8H4V7H5.5V5.5H6.5V7H8V8Z" fill="#FAFBFB"/>
+</svg>
diff --git a/src/gui/public.svg b/src/gui/public.svg
new file mode 100644 (file)
index 0000000..1407d57
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6 1C3.24 1 1 3.24 1 6C1 8.76 3.24 11 6 11C8.76 11 11 8.76 11 6C11 3.24 8.76 1 6 1ZM5.5 9.965C3.525 9.72 2 8.04 2 6C2 5.69 2.04 5.395 2.105 5.105L4.5 7.5V8C4.5 8.55 4.95 9 5.5 9V9.965ZM8.95 8.695C8.82 8.29 8.45 8 8 8H7.5V6.5C7.5 6.225 7.275 6 7 6H4V5H5C5.275 5 5.5 4.775 5.5 4.5V3.5H6.5C7.05 3.5 7.5 3.05 7.5 2.5V2.295C8.965 2.89 10 4.325 10 6C10 7.04 9.6 7.985 8.95 8.695Z" fill="#FAFBFB"/>
+</svg>
index 3330d2560559878e19431f28180bff29d507401c..b5a5c3f6850ea36039f73ca3fcb58ed3274ce850 100644 (file)
 #include <ctime>
 
 #include "about.h"
-#ifdef NO_JTVS
+#ifndef NO_VS
+#include "virtualstudio.h"
+#endif
+#ifdef PSI
 #include "ui_qjacktrip_novs.h"
 #else
 #include "ui_qjacktrip.h"
@@ -54,7 +57,7 @@
 
 QJackTrip::QJackTrip(int argc, QWidget* parent)
     : QMainWindow(parent)
-#ifdef NO_JTVS
+#ifdef PSI
     , m_ui(new Ui::QJackTrip)
 #else
     , m_ui(new Ui::QJackTripVS)
@@ -72,10 +75,6 @@ QJackTrip::QJackTrip(int argc, QWidget* parent)
 {
     m_ui->setupUi(this);
 
-    QCoreApplication::setOrganizationName(QStringLiteral("jacktrip"));
-    QCoreApplication::setOrganizationDomain(QStringLiteral("jacktrip.org"));
-    QCoreApplication::setApplicationName(QStringLiteral("JackTrip"));
-
     // Set up our debug window, and relay everything to our real cout.
     std::cout.rdbuf(m_debugDialog->getOutputStream()->rdbuf());
     std::cerr.rdbuf(m_debugDialog->getOutputStream(1)->rdbuf());
@@ -108,6 +107,10 @@ QJackTrip::QJackTrip(int argc, QWidget* parent)
         About about(this);
         about.exec();
     });
+#ifndef NO_VS
+    connect(m_ui->vsModeButton, &QPushButton::clicked, this,
+            &QJackTrip::virtualStudioMode);
+#endif
     connect(m_ui->autoPatchComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
             this, [=]() {
                 if (m_ui->autoPatchComboBox->currentIndex() == CLIENTFOFI
@@ -234,6 +237,7 @@ QJackTrip::QJackTrip(int argc, QWidget* parent)
     m_ui->autoPatchGroupBox->setVisible(false);
     m_ui->requireAuthGroupBox->setVisible(false);
     m_ui->backendWarningLabel->setVisible(false);
+    m_ui->vsModeButton->setVisible(false);
 
 #ifdef RT_AUDIO
     connect(m_ui->backendComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
@@ -324,7 +328,7 @@ QJackTrip::QJackTrip(int argc, QWidget* parent)
             "JACK was not found. This means that only the RtAudio backend is available "
             "and that JackTrip cannot be run in hub server mode.");
 
-#ifdef NO_JTVS
+#ifdef PSI
         QSettings settings;
         settings.beginGroup(QStringLiteral("Audio"));
         if (!settings.value(QStringLiteral("HideJackWarning"), false).toBool()) {
@@ -360,7 +364,7 @@ QJackTrip::QJackTrip(int argc, QWidget* parent)
             settings.setValue(QStringLiteral("UsingFallback"), false);
         }
         settings.endGroup();
-#endif  // NO_JTVS
+#endif  // PSI
 #else   // RT_AUDIO
         QMessageBox msgBox;
         msgBox.setText(
@@ -415,6 +419,37 @@ void QJackTrip::resizeEvent(QResizeEvent* event)
     m_ui->authDisclaimerLabel->setMinimumHeight(rect.height());
 }
 
+void QJackTrip::showEvent(QShowEvent* event)
+{
+    // We need to wait to load geometry until here rather than with our other settings.
+    // If we don't, the window geometry will be improperly set on macOS whenever the
+    // VirtualStudio window is shown first.
+    QMainWindow::showEvent(event);
+    if (m_firstShow) {
+        QSettings settings;
+        settings.beginGroup(QStringLiteral("Window"));
+        QByteArray geometry = settings.value(QStringLiteral("Geometry")).toByteArray();
+        if (geometry.size() > 0) {
+            restoreGeometry(geometry);
+        } else {
+            // Because of hidden elements in our dialog window, it's vertical size in the
+            // creator is getting rediculous. Set it to something sensible by default if
+            // this is our first load.
+            this->resize(QSize(this->size().height(), 600));
+        }
+        settings.endGroup();
+        m_firstShow = false;
+    }
+}
+
+#ifndef NO_VS
+void QJackTrip::setVs(QSharedPointer<VirtualStudio> vs)
+{
+    m_vs = vs;
+    m_ui->vsModeButton->setVisible(!m_vs.isNull());
+}
+#endif
+
 void QJackTrip::processFinished()
 {
     if (!m_jackTripRunning) {
@@ -444,7 +479,7 @@ void QJackTrip::processError(const QString& errorMessage)
 {
     QMessageBox msgBox;
     if (errorMessage == QLatin1String("Peer Stopped")) {
-        // Report the other end quitting as a regular occurance rather than an error.
+        // Report the other end quitting as a regular occurrence rather than an error.
         msgBox.setText(errorMessage);
         msgBox.setWindowTitle(QStringLiteral("Disconnected"));
     } else {
@@ -670,6 +705,7 @@ void QJackTrip::resetOptions()
     m_ui->realTimeCheckBox->setChecked(true);
     m_ui->ioStatsCheckBox->setChecked(false);
     m_ui->ioStatsSpinBox->setValue(1);
+    m_ui->verboseCheckBox->setChecked(false);
 
     saveSettings();
 }
@@ -943,6 +979,14 @@ void QJackTrip::exit()
     }
 }
 
+#ifndef NO_VS
+void QJackTrip::virtualStudioMode()
+{
+    this->hide();
+    m_vs->show();
+}
+#endif
+
 int QJackTrip::findTab(const QString& tabName)
 {
     for (int i = 0; i < m_ui->optionsTabWidget->count(); i++) {
@@ -1177,18 +1221,6 @@ void QJackTrip::loadSettings()
     m_ui->outClientsSpinBox->setValue(
         settings.value(QStringLiteral("Clients"), 1).toInt());
     settings.endGroup();
-
-    settings.beginGroup(QStringLiteral("Window"));
-    QByteArray geometry = settings.value(QStringLiteral("Geometry")).toByteArray();
-    if (geometry.size() > 0) {
-        restoreGeometry(geometry);
-    } else {
-        // Because of hidden elements in our dialog window, it's vertical size in the
-        // creator is getting rediculous. Set it to something sensible by default if this
-        // is our first load.
-        this->resize(QSize(this->size().width(), 600));
-    }
-    settings.endGroup();
 }
 
 void QJackTrip::saveSettings()
index 27528a7d096ceeb2cad38762e7dacb134b948f41..3410d651eb04e11231160adb69c5a0eb31515a1a 100644 (file)
 
 namespace Ui
 {
-#ifdef NO_JTVS
+#ifdef PSI
 class QJackTrip;
 #else
 class QJackTripVS;
 #endif
 }  // namespace Ui
 
+#ifndef NO_VS
+class VirtualStudio;
+#endif
+
 class QJackTrip : public QMainWindow
 {
     Q_OBJECT
@@ -68,6 +72,12 @@ class QJackTrip : public QMainWindow
 
     void closeEvent(QCloseEvent* event) override;
     void resizeEvent(QResizeEvent* event) override;
+    void showEvent(QShowEvent* event) override;
+
+#ifndef NO_VS
+    enum uiModeT{UNSET, VIRTUAL_STUDIO, STANDARD};
+    void setVs(QSharedPointer<VirtualStudio> vs);
+#endif
 
    signals:
     void signalExit();
@@ -88,6 +98,9 @@ class QJackTrip : public QMainWindow
     void start();
     void stop();
     void exit();
+#ifndef NO_VS
+    void virtualStudioMode();
+#endif
 
    private:
     enum runTypeT { P2P_CLIENT, P2P_SERVER, HUB_CLIENT, HUB_SERVER };
@@ -109,7 +122,7 @@ class QJackTrip : public QMainWindow
     QString commandLineFromCurrentOptions();
     void showCommandLineMessageBox();
 
-#ifdef NO_JTVS
+#ifdef PSI
     QScopedPointer<Ui::QJackTrip> m_ui;
 #else
     QScopedPointer<Ui::QJackTripVS> m_ui;
@@ -132,7 +145,11 @@ class QJackTrip : public QMainWindow
     QLabel m_autoQueueIndicator;
     int m_argc;
     bool m_hideWarning;
+    bool m_firstShow = true;
 
+#ifndef NO_VS
+    QSharedPointer<VirtualStudio> m_vs;
+#endif
 #ifdef __APPLE__
     NoNap m_noNap;
 #endif
index 179c85a0338656107e5fbea5fe12d4335cec3e66..ad8f86496f8b397f464e96c302b7ca7b0a425ed9 100644 (file)
@@ -4,4 +4,50 @@
     <file>about.png</file>
     <file>icon.png</file>
   </qresource>
+  <qresource prefix="vs">
+    <file>vs.qml</file>
+    <file>FirstLaunch.qml</file>
+    <file>Login.qml</file>
+    <file>Studio.qml</file>
+    <file>Browse.qml</file>
+    <file>Settings.qml</file>
+    <file>Connected.qml</file>
+    <file>Setup.qml</file>
+    <file>logo.svg</file>
+    <file>wedge.svg</file>
+    <file>wedge_inactive.svg</file>
+    <file>private.svg</file>
+    <file>public.svg</file>
+    <file>join.svg</file>
+    <file>leave.svg</file>
+    <file>cog.svg</file>
+    <file>mic.svg</file>
+    <file>ethernet.png</file>
+    <file>headphones.svg</file>
+    <file>jacktrip.png</file>
+    <file>jacktrip white.png</file>
+    <file>JTOriginal.png</file>
+    <file>JTVS.png</file>
+    <file>flags/AE.svg</file>
+    <file>flags/AU.svg</file>
+    <file>flags/BE.svg</file>
+    <file>flags/BR.svg</file>
+    <file>flags/CA.svg</file>
+    <file>flags/CH.svg</file>
+    <file>flags/DE.svg</file>
+    <file>flags/FR.svg</file>
+    <file>flags/GB.svg</file>
+    <file>flags/HK.svg</file>
+    <file>flags/ID.svg</file>
+    <file>flags/IT.svg</file>
+    <file>flags/JP.svg</file>
+    <file>flags/RO.svg</file>
+    <file>flags/SE.svg</file>
+    <file>flags/SG.svg</file>
+    <file>flags/TW.svg</file>
+    <file>flags/US.svg</file>
+    <file>flags/ZA.svg</file>
+    <file>Poppins-Bold.ttf</file>
+    <file>Poppins-Regular.ttf</file>
+  </qresource>
 </RCC>
index 52dc336979120f66dcb9a6fbe5945dc4d068d0fd..9044e47cf64e8942c72130707ac69b3bb69604f1 100644 (file)
@@ -308,6 +308,13 @@ To connect to a hub server you need to run as a hub client.</string>
             </property>
            </spacer>
           </item>
+          <item>
+           <widget class="QPushButton" name="vsModeButton">
+            <property name="text">
+             <string>Virtual Studio Mode</string>
+            </property>
+           </widget>
+          </item>
           <item>
            <widget class="QPushButton" name="aboutButton">
             <property name="text">
@@ -826,6 +833,9 @@ play from this machine. (Available in client fan out/in and full mix modes.)</st
           <property name="minimum">
            <number>2</number>
           </property>
+          <property name="maximum">
+           <number>999</number>
+          </property>
           <property name="value">
            <number>4</number>
           </property>
@@ -1470,6 +1480,12 @@ for better quality at the expense of latency.</string>
           <property name="toolTip">
            <string>Set the broadcast queue buffer length, in packet size.</string>
           </property>
+          <property name="minimum">
+           <number>2</number>
+          </property>
+          <property name="maximum">
+           <number>999</number>
+          </property>
           <property name="value">
            <number>8</number>
           </property>
@@ -1811,6 +1827,7 @@ and wetness is the essence of beauty.</string>
   <tabstop>keyBrowse</tabstop>
   <tabstop>credsEdit</tabstop>
   <tabstop>credsBrowse</tabstop>
+  <tabstop>vsModeButton</tabstop>
   <tabstop>aboutButton</tabstop>
   <tabstop>clientNameEdit</tabstop>
   <tabstop>remoteNameEdit</tabstop>
diff --git a/src/gui/qjacktrip_novs.qrc b/src/gui/qjacktrip_novs.qrc
new file mode 100644 (file)
index 0000000..179c85a
--- /dev/null
@@ -0,0 +1,7 @@
+<RCC>
+  <qresource prefix="qjacktrip">
+    <file>about@2x.png</file>
+    <file>about.png</file>
+    <file>icon.png</file>
+  </qresource>
+</RCC>
index 6e6ce34a39f5f197c97b7a10a951909a848e7e7b..7aaced209d481fdbbc700c0788c9ff9ffb072ff0 100644 (file)
@@ -572,6 +572,13 @@ play from this machine. (Available in client fan out/in and full mix modes.)</st
             </property>
            </spacer>
           </item>
+          <item>
+           <widget class="QPushButton" name="vsModeButton">
+            <property name="text">
+             <string>&amp;Virtual Studio Mode</string>
+            </property>
+           </widget>
+          </item>
           <item>
            <widget class="QPushButton" name="aboutButton">
             <property name="text">
@@ -977,6 +984,9 @@ play from this machine. (Available in client fan out/in and full mix modes.)</st
           <property name="minimum">
            <number>2</number>
           </property>
+          <property name="maximum">
+           <number>999</number>
+          </property>
           <property name="value">
            <number>4</number>
           </property>
@@ -1470,6 +1480,12 @@ for better quality at the expense of latency.</string>
           <property name="toolTip">
            <string>Set the broadcast queue buffer length, in packet size.</string>
           </property>
+          <property name="minimum">
+           <number>2</number>
+          </property>
+          <property name="maximum">
+           <number>999</number>
+          </property>
           <property name="value">
            <number>8</number>
           </property>
@@ -1814,6 +1830,7 @@ and wetness is the essence of beauty.</string>
   <tabstop>authCheckBox</tabstop>
   <tabstop>usernameEdit</tabstop>
   <tabstop>passwordEdit</tabstop>
+  <tabstop>vsModeButton</tabstop>
   <tabstop>aboutButton</tabstop>
   <tabstop>clientNameEdit</tabstop>
   <tabstop>remoteNameEdit</tabstop>
diff --git a/src/gui/virtualstudio.cpp b/src/gui/virtualstudio.cpp
new file mode 100644 (file)
index 0000000..93df669
--- /dev/null
@@ -0,0 +1,1223 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  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.
+*/
+//*****************************************************************
+
+/**
+ * \file virtualstudio.cpp
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#include "virtualstudio.h"
+
+#include <QDesktopServices>
+#include <QMessageBox>
+#include <QQmlContext>
+#include <QQmlEngine>
+#include <QSslSocket>
+#include <algorithm>
+#include <iostream>
+
+#include "../jacktrip_globals.h"
+#include "about.h"
+#include "qjacktrip.h"
+
+#ifdef USE_WEAK_JACK
+#include "weak_libjack.h"
+#endif
+#ifdef RT_AUDIO
+#include "RtAudio.h"
+#endif
+
+#ifdef _WIN32
+#include <wingdi.h>
+#endif
+
+VirtualStudio::VirtualStudio(bool firstRun, QObject* parent)
+    : QObject(parent), m_showFirstRun(firstRun)
+{
+    QSettings settings;
+    m_updateChannel =
+        settings.value(QStringLiteral("UpdateChannel"), "stable").toString().toLower();
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    m_refreshToken    = settings.value(QStringLiteral("RefreshToken"), "").toString();
+    m_userId          = settings.value(QStringLiteral("UserId"), "").toString();
+    m_uiScale         = settings.value(QStringLiteral("UiScale"), 1).toFloat();
+    m_darkMode        = settings.value(QStringLiteral("DarkMode"), false).toBool();
+    m_showInactive    = settings.value(QStringLiteral("ShowInactive"), false).toBool();
+    m_showSelfHosted  = settings.value(QStringLiteral("ShowSelfHosted"), false).toBool();
+    m_showDeviceSetup = settings.value(QStringLiteral("ShowDeviceSetup"), true).toBool();
+    m_showWarnings    = settings.value(QStringLiteral("ShowWarnings"), true).toBool();
+    settings.endGroup();
+    m_previousUiScale = m_uiScale;
+
+    // Load our font for our qml interface
+    QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Regular.ttf"));
+    QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Bold.ttf"));
+
+    connect(&m_view, &VsQuickView::windowClose, this, &VirtualStudio::exit);
+
+    // Set our font scaling to convert points to pixels
+    m_fontScale = 4.0 / 3.0;
+
+#ifdef RT_AUDIO
+    settings.beginGroup(QStringLiteral("Audio"));
+    m_useRtAudio   = settings.value(QStringLiteral("Backend"), 0).toInt() == 1;
+    m_inputDevice  = settings.value(QStringLiteral("InputDevice"), "").toString();
+    m_outputDevice = settings.value(QStringLiteral("OutputDevice"), "").toString();
+    m_bufferSize   = settings.value(QStringLiteral("BufferSize"), 128).toInt();
+    settings.endGroup();
+    m_previousBuffer = m_bufferSize;
+    refreshDevices();
+    m_previousInput  = m_inputDevice;
+    m_previousOutput = m_outputDevice;
+#else
+    m_selectableBackend = false;
+
+    // Set our combo box models to an empty list to avoid a reference error
+    m_view.engine()->rootContext()->setContextProperty(
+        QStringLiteral("inputComboModel"),
+        QVariant::fromValue(QStringList(QLatin1String(""))));
+    m_view.engine()->rootContext()->setContextProperty(
+        QStringLiteral("outputComboModel"),
+        QVariant::fromValue(QStringList(QLatin1String(""))));
+#endif
+
+#ifdef USE_WEAK_JACK
+    // Check if Jack is available
+    if (have_libjack() != 0) {
+#ifdef RT_AUDIO
+        m_useRtAudio        = true;
+        m_selectableBackend = false;
+#else
+        // TODO: Handle this more gracefully, even if it's an unlikely scenario
+        qFatal("JACK not found and not built with RtAudio support.");
+#endif  // RT_AUDIO
+    }
+#endif  // USE_WEAK_JACK
+#ifdef RT_AUDIO
+    m_previousUseRtAudio = m_useRtAudio;
+#endif
+
+    m_view.engine()->rootContext()->setContextProperty(
+        QStringLiteral("bufferComboModel"), QVariant::fromValue(m_bufferOptions));
+    m_view.engine()->rootContext()->setContextProperty(
+        QStringLiteral("updateChannelComboModel"),
+        QVariant::fromValue(m_updateChannelOptions));
+    m_view.engine()->rootContext()->setContextProperty(QStringLiteral("virtualstudio"),
+                                                       this);
+    m_view.engine()->rootContext()->setContextProperty(QStringLiteral("serverModel"),
+                                                       QVariant::fromValue(m_servers));
+    m_view.engine()->rootContext()->setContextProperty(
+        QStringLiteral("backendComboModel"),
+        QVariant::fromValue(QStringList()
+                            << QStringLiteral("JACK") << QStringLiteral("RtAudio")));
+    m_view.setSource(QUrl(QStringLiteral("qrc:/vs/vs.qml")));
+    m_view.setMinimumSize(QSize(594, 519));
+    // m_view.setMaximumSize(QSize(696, 577));
+    m_view.resize(696 * m_uiScale, 577 * m_uiScale);
+
+    // Connect our timers
+    connect(&m_startTimer, &QTimer::timeout, this, &VirtualStudio::checkForHostname);
+    connect(&m_retryPeriodTimer, &QTimer::timeout, this, &VirtualStudio::endRetryPeriod);
+    connect(&m_refreshTimer, &QTimer::timeout, this, [&]() {
+        m_refreshMutex.lock();
+        if (m_allowRefresh) {
+            m_refreshMutex.unlock();
+            emit periodicRefresh();
+        } else {
+            m_refreshMutex.unlock();
+        }
+    });
+}
+
+void VirtualStudio::setStandardWindow(QSharedPointer<QJackTrip> window)
+{
+    m_standardWindow = window;
+}
+
+void VirtualStudio::show()
+{
+    if (m_checkSsl) {
+        // Check our available SSL version
+        QString sslVersion = QSslSocket::sslLibraryVersionString();
+        std::cout << "SSL Library: " << sslVersion.toStdString() << std::endl;
+        if (sslVersion.isEmpty()) {
+            QMessageBox msgBox;
+            msgBox.setText(
+                QStringLiteral("OpenSSL was not found. You will not be able to connect "
+                               "to the Virtual Studio server."));
+            msgBox.setWindowTitle(QStringLiteral("SSL Error"));
+            msgBox.exec();
+        }
+        m_checkSsl = false;
+    }
+
+    if (!m_showFirstRun) {
+        toVirtualStudio();
+    }
+    m_view.show();
+}
+
+bool VirtualStudio::showFirstRun()
+{
+    return m_showFirstRun;
+}
+
+bool VirtualStudio::hasRefreshToken()
+{
+    return !m_refreshToken.isEmpty();
+}
+
+QString VirtualStudio::versionString()
+{
+    return QLatin1String(gVersion);
+}
+
+QString VirtualStudio::logoSection()
+{
+    return m_logoSection;
+}
+
+bool VirtualStudio::selectableBackend()
+{
+    return m_selectableBackend;
+}
+
+QString VirtualStudio::audioBackend()
+{
+    return m_useRtAudio ? QStringLiteral("RtAudio") : QStringLiteral("JACK");
+}
+
+void VirtualStudio::setAudioBackend(const QString& backend)
+{
+    if (!m_selectableBackend) {
+        return;
+    }
+    m_useRtAudio = (backend == QStringLiteral("RtAudio"));
+    emit audioBackendChanged();
+}
+
+int VirtualStudio::inputDevice()
+{
+#ifdef RT_AUDIO
+    if (m_useRtAudio) {
+        int index = m_inputDeviceList.indexOf(m_inputDevice);
+        return index >= 0 ? index : 0;
+    }
+#endif
+    return 0;
+}
+
+void VirtualStudio::setInputDevice([[maybe_unused]] int device)
+{
+    if (!m_useRtAudio) {
+        return;
+    }
+#ifdef RT_AUDIO
+    m_inputDevice = m_inputDeviceList.at(device);
+#endif
+}
+
+int VirtualStudio::outputDevice()
+{
+#ifdef RT_AUDIO
+    if (m_useRtAudio) {
+        int index = m_outputDeviceList.indexOf(m_outputDevice);
+        return index >= 0 ? index : 0;
+    }
+#endif
+    return 0;
+}
+
+void VirtualStudio::setOutputDevice([[maybe_unused]] int device)
+{
+    if (!m_useRtAudio) {
+        return;
+    }
+#ifdef RT_AUDIO
+    m_outputDevice = m_outputDeviceList.at(device);
+#endif
+}
+
+int VirtualStudio::bufferSize()
+{
+#ifdef RT_AUDIO
+    if (m_useRtAudio) {
+        int index = m_bufferOptions.indexOf(QString::number(m_bufferSize));
+        // It shouldn't be possible that our buffer size doesn't exists
+        // but default to 128 if something goes wrong.
+        return index >= 0 ? index : m_bufferOptions.indexOf(QStringLiteral("128"));
+    }
+#endif
+    return 3;
+}
+
+void VirtualStudio::setBufferSize([[maybe_unused]] int index)
+{
+    if (!m_useRtAudio) {
+        return;
+    }
+#ifdef RT_AUDIO
+    m_bufferSize = m_bufferOptions.at(index).toInt();
+#endif
+}
+
+int VirtualStudio::currentStudio()
+{
+    return m_currentStudio;
+}
+
+QString VirtualStudio::connectionState()
+{
+    return m_connectionState;
+}
+
+QString VirtualStudio::updateChannel()
+{
+    return m_updateChannel;
+}
+
+void VirtualStudio::setUpdateChannel(const QString& channel)
+{
+    m_updateChannel = channel;
+    QSettings settings;
+    settings.setValue(QStringLiteral("UpdateChannel"), m_updateChannel);
+    emit updateChannelChanged();
+}
+
+bool VirtualStudio::showInactive()
+{
+    return m_showInactive;
+}
+
+void VirtualStudio::setShowInactive(bool inactive)
+{
+    m_showInactive = inactive;
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("ShowInactive"), m_showInactive);
+    settings.endGroup();
+}
+
+bool VirtualStudio::showSelfHosted()
+{
+    return m_showSelfHosted;
+}
+
+void VirtualStudio::setShowSelfHosted(bool selfHosted)
+{
+    m_showSelfHosted = selfHosted;
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("ShowSelfHosted"), m_showSelfHosted);
+    settings.endGroup();
+}
+
+bool VirtualStudio::showDeviceSetup()
+{
+    return m_showDeviceSetup;
+}
+
+void VirtualStudio::setShowDeviceSetup(bool show)
+{
+    m_showDeviceSetup = show;
+}
+
+bool VirtualStudio::showWarnings()
+{
+    return m_showWarnings;
+}
+
+void VirtualStudio::setShowWarnings(bool show)
+{
+    m_showWarnings = show;
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("ShowWarnings"), m_showWarnings);
+    settings.endGroup();
+    emit showWarningsChanged();
+}
+
+float VirtualStudio::fontScale()
+{
+    return m_fontScale;
+}
+
+float VirtualStudio::uiScale()
+{
+    return m_uiScale;
+}
+
+void VirtualStudio::setUiScale(float scale)
+{
+    m_uiScale = scale;
+    emit uiScaleChanged();
+}
+
+bool VirtualStudio::darkMode()
+{
+    return m_darkMode;
+}
+
+void VirtualStudio::setDarkMode(bool dark)
+{
+    m_darkMode = dark;
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("DarkMode"), m_darkMode);
+    settings.endGroup();
+    emit darkModeChanged();
+}
+
+bool VirtualStudio::noUpdater()
+{
+#ifdef NO_UPDATER
+    return true;
+#else
+    return false;
+#endif
+}
+
+bool VirtualStudio::psiBuild()
+{
+#ifdef PSI
+    return true;
+#else
+    return false;
+#endif
+}
+
+void VirtualStudio::toStandard()
+{
+    if (!m_standardWindow.isNull()) {
+        m_view.hide();
+        m_standardWindow->show();
+    }
+    QSettings settings;
+    settings.setValue(QStringLiteral("UiMode"), QJackTrip::STANDARD);
+    m_refreshTimer.stop();
+
+    if (m_showFirstRun) {
+        m_showFirstRun = false;
+        emit showFirstRunChanged();
+    }
+}
+
+void VirtualStudio::toVirtualStudio()
+{
+    if (!m_refreshToken.isEmpty()) {
+        // Attempt to refresh our virtual studio auth token
+        setupAuthenticator();
+
+        // Something about this is required for refreshing auth tokens:
+        // https://bugreports.qt.io/browse/QTBUG-84866
+        m_authenticator->setModifyParametersFunction([](QAbstractOAuth2::Stage stage,
+                                                        QVariantMap* parameters) {
+            if (stage == QAbstractOAuth2::Stage::RequestingAccessToken) {
+                QByteArray code = parameters->value(QStringLiteral("code")).toByteArray();
+                (*parameters)[QStringLiteral("code")] = QUrl::fromPercentEncoding(code);
+            } else if (stage == QAbstractOAuth2::Stage::RequestingAuthorization) {
+                parameters->insert(QStringLiteral("audience"),
+                                   QStringLiteral("https://api.jacktrip.org"));
+            }
+            if (!parameters->contains("client_id")) {
+                parameters->insert("client_id", "cROUJag0UVKDaJ6jRAKRzlVjKVFNU39I");
+            }
+        });
+
+        m_authenticator->setRefreshToken(m_refreshToken);
+        m_authenticator->refreshAccessToken();
+    }
+}
+
+void VirtualStudio::login()
+{
+    setupAuthenticator();
+    m_authenticator->grant();
+}
+
+void VirtualStudio::logout()
+{
+    m_authenticator->setToken(QLatin1String(""));
+    m_authenticator->setRefreshToken(QLatin1String(""));
+
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.remove(QStringLiteral("RefreshToken"));
+    settings.remove(QStringLiteral("UserId"));
+    settings.endGroup();
+
+    m_refreshTimer.stop();
+
+    m_refreshToken.clear();
+    m_userId.clear();
+    emit hasRefreshTokenChanged();
+}
+
+void VirtualStudio::refreshStudios(int index)
+{
+    getServerList(false, index);
+}
+
+void VirtualStudio::refreshDevices()
+{
+#ifdef RT_AUDIO
+    getDeviceList(&m_inputDeviceList, true);
+    getDeviceList(&m_outputDeviceList, false);
+    m_view.engine()->rootContext()->setContextProperty(
+        QStringLiteral("inputComboModel"), QVariant::fromValue(m_inputDeviceList));
+    m_view.engine()->rootContext()->setContextProperty(
+        QStringLiteral("outputComboModel"), QVariant::fromValue(m_outputDeviceList));
+
+    // Make sure we keep our current settings if the device still exists
+    if (!m_inputDeviceList.contains(m_inputDevice)) {
+        m_inputDevice = QStringLiteral("(default)");
+    }
+    if (!m_outputDeviceList.contains(m_outputDevice)) {
+        m_outputDevice = QStringLiteral("(default)");
+    }
+
+    emit inputDeviceChanged();
+    emit outputDeviceChanged();
+#endif
+}
+
+void VirtualStudio::revertSettings()
+{
+    m_uiScale = m_previousUiScale;
+    emit uiScaleChanged();
+#ifdef RT_AUDIO
+    // Restore our previous settings
+    m_inputDevice  = m_previousInput;
+    m_outputDevice = m_previousOutput;
+    m_bufferSize   = m_previousBuffer;
+    m_useRtAudio   = m_previousUseRtAudio;
+    emit inputDeviceChanged();
+    emit outputDeviceChanged();
+    emit bufferSizeChanged();
+    emit audioBackendChanged();
+#endif
+}
+
+void VirtualStudio::applySettings()
+{
+    m_previousUiScale = m_uiScale;
+    emit newScale();
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("UiScale"), m_uiScale);
+    settings.setValue(QStringLiteral("ShowDeviceSetup"), m_showDeviceSetup);
+    settings.endGroup();
+#ifdef RT_AUDIO
+    settings.beginGroup(QStringLiteral("Audio"));
+    settings.setValue(QStringLiteral("Backend"), m_useRtAudio ? 1 : 0);
+    settings.setValue(QStringLiteral("BufferSize"), m_bufferSize);
+    settings.setValue(QStringLiteral("InputDevice"), m_inputDevice);
+    settings.setValue(QStringLiteral("OutputDevice"), m_outputDevice);
+    settings.endGroup();
+
+    m_previousUseRtAudio = m_useRtAudio;
+    m_previousBuffer     = m_bufferSize;
+    m_previousInput      = m_inputDevice;
+    m_previousOutput     = m_outputDevice;
+
+    emit inputDeviceChanged();
+    emit outputDeviceChanged();
+#endif
+}
+
+void VirtualStudio::connectToStudio(int studioIndex)
+{
+    {
+        QMutexLocker locker(&m_refreshMutex);
+        m_allowRefresh = false;
+    }
+    m_refreshTimer.stop();
+
+    m_currentStudio          = studioIndex;
+    VsServerInfo* studioInfo = static_cast<VsServerInfo*>(m_servers.at(m_currentStudio));
+    emit currentStudioChanged();
+    m_onConnectedScreen = true;
+
+    // Check if we have an address for our server
+    if (studioInfo->host().isEmpty()) {
+        // EXPERIMENTAL CODE. (It shouldn't be possible to arrive here.)
+        if (studioInfo->isManageable()) {
+            m_connectionState = QStringLiteral("Starting Studio...");
+            emit connectionStateChanged();
+
+            // Send a put request to start our studio
+            m_startedStudio = true;
+            QString expiration =
+                QDateTime::currentDateTimeUtc().addSecs(60 * 60).toString(Qt::ISODate);
+            QJsonObject json      = {{QLatin1String("enabled"), true},
+                                {QLatin1String("expiresAt"), expiration}};
+            QJsonDocument request = QJsonDocument(json);
+
+            QNetworkReply* reply = m_authenticator->put(
+                QStringLiteral("https://app.jacktrip.org/api/servers/%1")
+                    .arg(studioInfo->id()),
+                request.toJson());
+            connect(reply, &QNetworkReply::finished, this, [&, reply]() {
+                if (reply->error() != QNetworkReply::NoError) {
+                    m_connectionState = QStringLiteral("Unable to Start Studio");
+                    emit connectionStateChanged();
+                } else {
+                    QByteArray response       = reply->readAll();
+                    QJsonDocument serverState = QJsonDocument::fromJson(response);
+                    if (serverState.object()[QStringLiteral("status")].toString()
+                        == QLatin1String("Starting")) {
+                        // Start our timer to check for our hostname
+                        m_startTimer.setInterval(5000);
+                        m_startTimer.start();
+                    }
+                }
+                reply->deleteLater();
+            });
+        } else {
+            m_connectionState = QStringLiteral("Unable to Start Studio");
+            emit connectionStateChanged();
+            m_startedStudio = false;
+        }
+    } else {
+        m_startedStudio = false;
+        completeConnection();
+    }
+}
+
+void VirtualStudio::completeConnection()
+{
+    if (m_currentStudio < 0) {
+        return;
+    }
+
+    m_jackTripRunning = true;
+    m_connectionState = QStringLiteral("Connecting...");
+    emit connectionStateChanged();
+    VsServerInfo* studioInfo = static_cast<VsServerInfo*>(m_servers.at(m_currentStudio));
+    try {
+        m_jackTrip.reset(new JackTrip(JackTrip::CLIENTTOPINGSERVER, JackTrip::UDP, 2, 2,
+#ifdef WAIR  // wair
+                                      0,
+#endif  // endwhere
+                                      4, 1));
+        m_jackTrip->setConnectDefaultAudioPorts(true);
+#ifdef RT_AUDIO
+        if (m_useRtAudio) {
+            m_jackTrip->setAudiointerfaceMode(JackTrip::RTAUDIO);
+            m_jackTrip->setSampleRate(studioInfo->sampleRate());
+            m_jackTrip->setAudioBufferSizeInSamples(m_bufferSize);
+            if (m_inputDevice == QLatin1String("(default)")) {
+                m_jackTrip->setInputDevice("");
+            } else {
+                m_jackTrip->setInputDevice(m_inputDevice.toStdString());
+            }
+            if (m_outputDevice == QLatin1String("(default)")) {
+                m_jackTrip->setOutputDevice("");
+            } else {
+                m_jackTrip->setOutputDevice(m_outputDevice.toStdString());
+            }
+        }
+#endif
+        m_jackTrip->setBufferStrategy(1);
+        m_jackTrip->setBufferQueueLength(-500);
+        m_jackTrip->setPeerAddress(studioInfo->host());
+        m_jackTrip->setPeerPorts(studioInfo->port());
+        m_jackTrip->setPeerHandshakePort(studioInfo->port());
+
+        QObject::connect(m_jackTrip.data(), &JackTrip::signalProcessesStopped, this,
+                         &VirtualStudio::processFinished, Qt::QueuedConnection);
+        QObject::connect(m_jackTrip.data(), &JackTrip::signalError, this,
+                         &VirtualStudio::processError, Qt::QueuedConnection);
+        QObject::connect(m_jackTrip.data(), &JackTrip::signalReceivedConnectionFromPeer,
+                         this, &VirtualStudio::receivedConnectionFromPeer,
+                         Qt::QueuedConnection);
+
+        // TODO: replace the following:
+        // m_ui->statusBar->showMessage(QStringLiteral("Waiting for Peer..."));
+        /*
+        QObject::connect(m_jackTrip.data(), &JackTrip::signalUdpWaitingTooLong, this,
+                            &QJackTrip::udpWaitingTooLong, Qt::QueuedConnection);
+        QObject::connect(m_jackTrip.data(), &JackTrip::signalQueueLengthChanged, this,
+                            &QJackTrip::queueLengthChanged, Qt::QueuedConnection);*/
+
+#ifdef WAIRTOHUB                      // WAIR
+        m_jackTrip->startProcess(0);  // for WAIR compatibility, ID in jack client name
+#else
+        m_jackTrip->startProcess();
+#endif  // endwhere
+    } catch (const std::exception& e) {
+        // Let the user know what our exception was.
+        m_connectionState = QStringLiteral("JackTrip Error");
+        emit connectionStateChanged();
+
+        QMessageBox msgBox;
+        msgBox.setText(QStringLiteral("Error: ").append(e.what()));
+        msgBox.setWindowTitle(QStringLiteral("Doh!"));
+        msgBox.exec();
+
+        m_jackTripRunning = false;
+        emit disconnected();
+        m_onConnectedScreen = false;
+        return;
+    }
+
+#ifdef __APPLE__
+    m_noNap.disableNap();
+#endif
+}
+
+void VirtualStudio::disconnect()
+{
+    m_connectionState = QStringLiteral("Disconnecting...");
+    emit connectionStateChanged();
+    m_retryPeriodTimer.stop();
+    m_retryPeriod = false;
+
+    if (m_jackTripRunning) {
+        if (m_startedStudio) {
+            VsServerInfo* studioInfo =
+                static_cast<VsServerInfo*>(m_servers.at(m_currentStudio));
+            QMessageBox msgBox;
+            msgBox.setText(QStringLiteral("Do you want to stop the current studio?"));
+            msgBox.setWindowTitle(QStringLiteral("Stop Studio"));
+            msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
+            msgBox.setDefaultButton(QMessageBox::Yes);
+            int ret = msgBox.exec();
+            if (ret == QMessageBox::Yes) {
+                studioInfo->setHost(QLatin1String(""));
+                stopStudio();
+            }
+        }
+        m_jackTrip->stop();
+    } else if (m_startedStudio) {
+        m_startTimer.stop();
+        stopStudio();
+        if (!m_isExiting) {
+            emit disconnected();
+            m_onConnectedScreen = false;
+        }
+    } else {
+        // How did we get here? This shouldn't be possible, but include for safety.
+        if (m_isExiting) {
+            emit signalExit();
+        } else {
+            emit disconnected();
+            m_onConnectedScreen = false;
+        }
+    }
+
+    // Restart our studio refresh timer.
+    if (!m_isExiting) {
+        QMutexLocker locker(&m_refreshMutex);
+        m_allowRefresh = true;
+        m_refreshTimer.start();
+    }
+}
+
+void VirtualStudio::manageStudio(int studioIndex)
+{
+    if (studioIndex == -1) {
+        // We're here from a connected screen. Use our current studio.
+        studioIndex = m_currentStudio;
+    }
+    QUrl url =
+        QUrl(QStringLiteral("https://app.jacktrip.org/studios/%1")
+                 .arg(static_cast<VsServerInfo*>(m_servers.at(studioIndex))->id()));
+    QDesktopServices::openUrl(url);
+}
+
+void VirtualStudio::createStudio()
+{
+    QUrl url = QUrl(QStringLiteral("https://app.jacktrip.org/studios/create"));
+    QDesktopServices::openUrl(url);
+}
+
+void VirtualStudio::showAbout()
+{
+    About about;
+    about.exec();
+}
+
+void VirtualStudio::exit()
+{
+    m_refreshTimer.stop();
+    if (m_onConnectedScreen) {
+        m_isExiting = true;
+        disconnect();
+    } else {
+        emit signalExit();
+    }
+}
+
+void VirtualStudio::slotAuthSucceded()
+{
+    m_refreshToken = m_authenticator->refreshToken();
+    emit hasRefreshTokenChanged();
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("RefreshToken"), m_refreshToken);
+    settings.endGroup();
+
+    settings.setValue(QStringLiteral("UiMode"), QJackTrip::VIRTUAL_STUDIO);
+
+    if (m_userId.isEmpty()) {
+        getUserId();
+    } else {
+        getSubscriptions();
+    }
+}
+
+void VirtualStudio::slotAuthFailed()
+{
+    emit authFailed();
+}
+
+void VirtualStudio::processFinished()
+{
+    if (m_isExiting) {
+        emit signalExit();
+        return;
+    }
+
+    if (m_retryPeriod && m_startedStudio) {
+        // Retry if necessary.
+        completeConnection();
+        return;
+    }
+
+    if (!m_jackTripRunning) {
+        return;
+    }
+
+    m_jackTripRunning = false;
+    m_connectionState = QStringLiteral("Disconnected");
+    m_jackTrip.reset();
+    emit connectionStateChanged();
+    emit disconnected();
+    m_onConnectedScreen = false;
+#ifdef __APPLE__
+    m_noNap.enableNap();
+#endif
+}
+
+void VirtualStudio::processError(const QString& errorMessage)
+{
+    if (!m_retryPeriod) {
+        QMessageBox msgBox;
+        if (errorMessage == QLatin1String("Peer Stopped")) {
+            // Report the other end quitting as a regular occurance rather than an error.
+            msgBox.setText(errorMessage);
+            msgBox.setWindowTitle(QStringLiteral("Disconnected"));
+        } else {
+            msgBox.setText(QStringLiteral("Error: ").append(errorMessage));
+            msgBox.setWindowTitle(QStringLiteral("Doh!"));
+        }
+        msgBox.exec();
+    }
+    processFinished();
+}
+
+void VirtualStudio::receivedConnectionFromPeer()
+{
+    m_connectionState = QStringLiteral("Connected");
+    emit connectionStateChanged();
+    std::cout << "Received connection" << std::endl;
+    emit connected();
+}
+
+void VirtualStudio::checkForHostname()
+{
+    if (m_currentStudio < 0) {
+        return;
+    }
+
+    VsServerInfo* studioInfo = static_cast<VsServerInfo*>(m_servers.at(m_currentStudio));
+    QNetworkReply* reply     = m_authenticator->get(
+            QStringLiteral("https://app.jacktrip.org/api/servers/%1").arg(studioInfo->id()));
+    connect(reply, &QNetworkReply::finished, this, [&, reply, studioInfo]() {
+        if (reply->error() != QNetworkReply::NoError) {
+            m_connectionState = QStringLiteral("Unable to Start Studio");
+            emit connectionStateChanged();
+
+            // Stop our timer
+            m_startTimer.stop();
+        } else {
+            QByteArray response       = reply->readAll();
+            QJsonDocument serverState = QJsonDocument::fromJson(response);
+            if (serverState.object()[QStringLiteral("status")].toString()
+                == QLatin1String("Ready")) {
+                // Ready to connect
+                m_startTimer.stop();
+                studioInfo->setHost(
+                    serverState.object()[QStringLiteral("serverHost")].toString());
+                studioInfo->setPort(
+                    serverState.object()[QStringLiteral("serverPort")].toInt());
+                m_retryPeriod = true;
+                m_retryPeriodTimer.setInterval(15000);
+                m_retryPeriodTimer.start();
+                completeConnection();
+            }
+        }
+        reply->deleteLater();
+        ;
+    });
+}
+
+void VirtualStudio::endRetryPeriod()
+{
+    m_retryPeriod = false;
+    m_retryPeriodTimer.stop();
+}
+
+void VirtualStudio::launchBrowser(const QUrl& url)
+{
+    std::cout << "Launching Browser" << std::endl;
+    bool success = QDesktopServices::openUrl(url);
+    if (success) {
+        std::cout << "Success" << std::endl;
+    } else {
+        std::cout << "Unable to open URL" << std::endl;
+    }
+}
+
+void VirtualStudio::setupAuthenticator()
+{
+    if (m_authenticator.isNull()) {
+        // Set up our authorization flow
+        m_authenticator.reset(new QOAuth2AuthorizationCodeFlow);
+        m_authenticator->setScope(
+            QStringLiteral("openid profile email offline_access read:servers"));
+        connect(m_authenticator.data(),
+                &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this,
+                &VirtualStudio::launchBrowser);
+
+        const QUrl authUri(QStringLiteral("https://auth.jacktrip.org/authorize"));
+        const QString clientId = QStringLiteral("cROUJag0UVKDaJ6jRAKRzlVjKVFNU39I");
+        const QUrl tokenUri(QStringLiteral("https://auth.jacktrip.org/oauth/token"));
+        const quint16 port = 52424;
+
+        m_authenticator->setAuthorizationUrl(authUri);
+        m_authenticator->setClientIdentifier(clientId);
+        m_authenticator->setAccessTokenUrl(tokenUri);
+
+        m_authenticator->setModifyParametersFunction([](QAbstractOAuth2::Stage stage,
+                                                        QVariantMap* parameters) {
+            if (stage == QAbstractOAuth2::Stage::RequestingAccessToken) {
+                QByteArray code = parameters->value(QStringLiteral("code")).toByteArray();
+                (*parameters)[QStringLiteral("code")] = QUrl::fromPercentEncoding(code);
+            } else if (stage == QAbstractOAuth2::Stage::RequestingAuthorization) {
+                parameters->insert(QStringLiteral("audience"),
+                                   QStringLiteral("https://api.jacktrip.org"));
+            }
+        });
+
+        QOAuthHttpServerReplyHandler* replyHandler =
+            new QOAuthHttpServerReplyHandler(port, this);
+        replyHandler->setCallbackText(QStringLiteral(
+            "<div id=\"container\" style=\"width:100%; max-width:1200px; height: auto; "
+            "margin: 100px auto; text-align:center;\">\n"
+            "<img src=\"https://files.jacktrip.org/logos/jacktrip_icon.svg\" "
+            "alt=\"JackTrip\">\n"
+            "<h1 style=\"font-size: 30px; font-weight: 600; padding-top:20px;\">Virtual "
+            "Studio Login Successful</h1>\n"
+            "<p style=\"font-size: 21px; font-weight:300;\">You may close this window "
+            "and return to the JackTrip application.</p>\n"
+            "</div>\n"));
+        m_authenticator->setReplyHandler(replyHandler);
+        connect(m_authenticator.data(), &QOAuth2AuthorizationCodeFlow::granted, this,
+                &VirtualStudio::slotAuthSucceded);
+        connect(m_authenticator.data(), &QOAuth2AuthorizationCodeFlow::requestFailed,
+                this, &VirtualStudio::slotAuthFailed);
+    }
+}
+
+void VirtualStudio::getServerList(bool firstLoad, int index)
+{
+    {
+        QMutexLocker locker(&m_refreshMutex);
+        if (!m_allowRefresh || m_refreshInProgress) {
+            return;
+        } else {
+            m_refreshInProgress = true;
+        }
+    }
+
+    // Get the serverId of the server at the top of our screen if we know it
+    QString topServerId;
+    if (index >= 0 && index < m_servers.count()) {
+        topServerId = static_cast<VsServerInfo*>(m_servers.at(index))->id();
+    }
+
+    QNetworkReply* reply =
+        m_authenticator->get(QStringLiteral("https://app.jacktrip.org/api/servers"));
+    connect(reply, &QNetworkReply::finished, this, [&, reply, topServerId, firstLoad]() {
+        if (reply->error() != QNetworkReply::NoError) {
+            std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+            emit authFailed();
+            reply->deleteLater();
+            return;
+        }
+
+        QByteArray response      = reply->readAll();
+        QJsonDocument serverList = QJsonDocument::fromJson(response);
+        if (!serverList.isArray()) {
+            std::cout << "Error: Not an array" << std::endl;
+            QMutexLocker locker(&m_refreshMutex);
+            m_refreshInProgress = false;
+            emit authFailed();
+            reply->deleteLater();
+            return;
+        }
+        QJsonArray servers = serverList.array();
+        // Divide our servers by category initially so that they're easier to sort
+        QList<QObject*> yourServers;
+        QList<QObject*> subServers;
+        QList<QObject*> pubServers;
+
+        for (int i = 0; i < servers.count(); i++) {
+            if (servers.at(i)[QStringLiteral("type")].toString().contains(
+                    QStringLiteral("JackTrip"))) {
+                VsServerInfo* serverInfo = new VsServerInfo(this);
+                serverInfo->setIsManageable(
+                    servers.at(i)[QStringLiteral("admin")].toBool());
+                QString status    = servers.at(i)[QStringLiteral("status")].toString();
+                bool activeStudio = status == QLatin1String("Ready");
+                bool hostedStudio = servers.at(i)[QStringLiteral("managed")].toBool();
+                // Only iterate through servers that we want to show
+                if (!m_showSelfHosted && !hostedStudio) {
+                    continue;
+                }
+                if (!m_showInactive && !activeStudio) {
+                    continue;
+                }
+                if (activeStudio || (serverInfo->isManageable() && m_showInactive)) {
+                    serverInfo->setName(servers.at(i)[QStringLiteral("name")].toString());
+                    serverInfo->setHost(
+                        servers.at(i)[QStringLiteral("serverHost")].toString());
+                    serverInfo->setStatus(
+                        servers.at(i)[QStringLiteral("status")].toString());
+                    serverInfo->setPort(
+                        servers.at(i)[QStringLiteral("serverPort")].toInt());
+                    serverInfo->setIsPublic(
+                        servers.at(i)[QStringLiteral("public")].toBool());
+                    serverInfo->setRegion(
+                        servers.at(i)[QStringLiteral("region")].toString());
+                    serverInfo->setPeriod(
+                        servers.at(i)[QStringLiteral("period")].toInt());
+                    serverInfo->setSampleRate(
+                        servers.at(i)[QStringLiteral("sampleRate")].toInt());
+                    serverInfo->setQueueBuffer(
+                        servers.at(i)[QStringLiteral("queueBuffer")].toInt());
+                    serverInfo->setId(servers.at(i)[QStringLiteral("id")].toString());
+                    if (servers.at(i)[QStringLiteral("owner")].toBool()) {
+                        yourServers.append(serverInfo);
+                        serverInfo->setSection(VsServerInfo::YOUR_STUDIOS);
+                    } else if (m_subscribedServers.contains(serverInfo->id())) {
+                        subServers.append(serverInfo);
+                        serverInfo->setSection(VsServerInfo::SUBSCRIBED_STUDIOS);
+                    } else {
+                        pubServers.append(serverInfo);
+                    }
+                }
+            }
+        }
+
+        std::sort(yourServers.begin(), yourServers.end(),
+                  [](QObject* first, QObject* second) {
+                      return static_cast<VsServerInfo*>(first)->name()
+                             < static_cast<VsServerInfo*>(second)->name();
+                  });
+        std::sort(subServers.begin(), subServers.end(),
+                  [](QObject* first, QObject* second) {
+                      return static_cast<VsServerInfo*>(first)->name()
+                             < static_cast<VsServerInfo*>(second)->name();
+                  });
+        std::sort(pubServers.begin(), pubServers.end(),
+                  [](QObject* first, QObject* second) {
+                      return static_cast<VsServerInfo*>(first)->name()
+                             < static_cast<VsServerInfo*>(second)->name();
+                  });
+
+        // If we don't have any owned servers, move the JackTrip logo to an appropriate
+        // section header.
+        if (yourServers.isEmpty()) {
+            if (subServers.isEmpty()) {
+                m_logoSection = QStringLiteral("Public Studios");
+            } else {
+                m_logoSection = QStringLiteral("Subscribed Studios");
+            }
+            emit logoSectionChanged();
+        } else {
+            m_logoSection = QStringLiteral("Your Studios");
+            emit logoSectionChanged();
+        }
+
+        QMutexLocker locker(&m_refreshMutex);
+        // Check that we haven't tried connecting to a server between the
+        // request going out and the response.
+        if (!m_allowRefresh) {
+            m_refreshInProgress = false;
+            return;
+        }
+        m_servers.clear();
+        m_servers.append(yourServers);
+        m_servers.append(subServers);
+        m_servers.append(pubServers);
+        m_view.engine()->rootContext()->setContextProperty(
+            QStringLiteral("serverModel"), QVariant::fromValue(m_servers));
+        int index = -1;
+        if (!topServerId.isEmpty()) {
+            for (int i = 0; i < m_servers.count(); i++) {
+                if (static_cast<VsServerInfo*>(m_servers.at(i))->id() == topServerId) {
+                    index = i;
+                    break;
+                }
+            }
+        }
+        if (firstLoad) {
+            emit authSucceeded();
+            m_refreshTimer.setInterval(10000);
+            m_refreshTimer.start();
+        } else {
+            emit refreshFinished(index);
+        }
+        m_refreshInProgress = false;
+
+        reply->deleteLater();
+    });
+}
+
+void VirtualStudio::getUserId()
+{
+    QNetworkReply* reply =
+        m_authenticator->get(QStringLiteral("https://auth.jacktrip.org/userinfo"));
+    connect(reply, &QNetworkReply::finished, this, [=]() {
+        if (reply->error() != QNetworkReply::NoError) {
+            std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+            emit authFailed();
+            reply->deleteLater();
+            return;
+        }
+
+        QByteArray response    = reply->readAll();
+        QJsonDocument userInfo = QJsonDocument::fromJson(response);
+        m_userId               = userInfo.object()[QStringLiteral("sub")].toString();
+
+        QSettings settings;
+        settings.beginGroup(QStringLiteral("VirtualStudio"));
+        settings.setValue(QStringLiteral("UserId"), m_userId);
+        settings.endGroup();
+        getSubscriptions();
+        reply->deleteLater();
+    });
+}
+
+void VirtualStudio::getSubscriptions()
+{
+    QNetworkReply* reply = m_authenticator->get(
+        QStringLiteral("https://app.jacktrip.org/api/users/%1/subscriptions")
+            .arg(m_userId));
+    connect(reply, &QNetworkReply::finished, this, [&, reply]() {
+        if (reply->error() != QNetworkReply::NoError) {
+            std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+            emit authFailed();
+            reply->deleteLater();
+            return;
+        }
+
+        QByteArray response            = reply->readAll();
+        QJsonDocument subscriptionList = QJsonDocument::fromJson(response);
+        if (!subscriptionList.isArray()) {
+            std::cout << "Error: Not an array" << std::endl;
+            emit authFailed();
+            reply->deleteLater();
+            return;
+        }
+        QJsonArray subscriptions = subscriptionList.array();
+        for (int i = 0; i < subscriptions.count(); i++) {
+            m_subscribedServers.append(
+                subscriptions.at(i)[QStringLiteral("serverId")].toString());
+        }
+        getServerList(true);
+        reply->deleteLater();
+    });
+}
+
+#ifdef RT_AUDIO
+void VirtualStudio::getDeviceList(QStringList* list, bool isInput)
+{
+    RtAudio audio;
+    list->clear();
+    list->append(QStringLiteral("(default)"));
+
+    unsigned int devices = audio.getDeviceCount();
+    RtAudio::DeviceInfo info;
+    for (unsigned int i = 0; i < devices; i++) {
+        info = audio.getDeviceInfo(i);
+        if (info.probed == true) {
+            if (isInput && info.inputChannels > 0) {
+                list->append(QString::fromStdString(info.name));
+            } else if (!isInput && info.outputChannels > 0) {
+                list->append(QString::fromStdString(info.name));
+            }
+        }
+    }
+}
+#endif
+
+void VirtualStudio::stopStudio()
+{
+    if (m_currentStudio < 0) {
+        return;
+    }
+
+    VsServerInfo* studioInfo = static_cast<VsServerInfo*>(m_servers.at(m_currentStudio));
+    QJsonObject json         = {{QLatin1String("enabled"), false}};
+    QJsonDocument request    = QJsonDocument(json);
+    studioInfo->setHost(QLatin1String(""));
+    QNetworkReply* reply = m_authenticator->put(
+        QStringLiteral("https://app.jacktrip.org/api/servers/%1").arg(studioInfo->id()),
+        request.toJson());
+    connect(reply, &QNetworkReply::finished, this, [=]() {
+        if (m_isExiting && !m_jackTripRunning) {
+            emit signalExit();
+        }
+        reply->deleteLater();
+    });
+}
+
+VirtualStudio::~VirtualStudio()
+{
+    for (int i = 0; i < m_servers.count(); i++) {
+        delete m_servers.at(i);
+    }
+}
diff --git a/src/gui/virtualstudio.h b/src/gui/virtualstudio.h
new file mode 100644 (file)
index 0000000..d6b2446
--- /dev/null
@@ -0,0 +1,253 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  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.
+*/
+//*****************************************************************
+
+/**
+ * \file virtualstudio.h
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#ifndef VIRTUALSTUDIO_H
+#define VIRTUALSTUDIO_H
+
+#include <QList>
+#include <QMutex>
+#include <QScopedPointer>
+#include <QSharedPointer>
+#include <QTimer>
+#include <QtNetworkAuth>
+
+#include "../JackTrip.h"
+#include "vsQuickView.h"
+#include "vsServerInfo.h"
+
+#ifdef __APPLE__
+#include "NoNap.h"
+#endif
+
+class QJackTrip;
+
+class VirtualStudio : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(bool showFirstRun READ showFirstRun NOTIFY showFirstRunChanged)
+    Q_PROPERTY(bool hasRefreshToken READ hasRefreshToken NOTIFY hasRefreshTokenChanged)
+    Q_PROPERTY(QString versionString READ versionString CONSTANT)
+    Q_PROPERTY(QString logoSection READ logoSection NOTIFY logoSectionChanged)
+    Q_PROPERTY(bool selectableBackend READ selectableBackend CONSTANT)
+    Q_PROPERTY(QString audioBackend READ audioBackend WRITE setAudioBackend NOTIFY
+                   audioBackendChanged)
+    Q_PROPERTY(
+        int inputDevice READ inputDevice WRITE setInputDevice NOTIFY inputDeviceChanged)
+    Q_PROPERTY(int outputDevice READ outputDevice WRITE setOutputDevice NOTIFY
+                   outputDeviceChanged)
+    Q_PROPERTY(
+        int bufferSize READ bufferSize WRITE setBufferSize NOTIFY bufferSizeChanged)
+    Q_PROPERTY(int currentStudio READ currentStudio NOTIFY currentStudioChanged)
+    Q_PROPERTY(bool showInactive READ showInactive WRITE setShowInactive NOTIFY
+                   showInactiveChanged)
+    Q_PROPERTY(bool showSelfHosted READ showSelfHosted WRITE setShowSelfHosted NOTIFY
+                   showSelfHostedChanged)
+    Q_PROPERTY(QString connectionState READ connectionState NOTIFY connectionStateChanged)
+    Q_PROPERTY(QString updateChannel READ updateChannel WRITE setUpdateChannel NOTIFY
+                   updateChannelChanged)
+    Q_PROPERTY(float fontScale READ fontScale CONSTANT)
+    Q_PROPERTY(float uiScale READ uiScale WRITE setUiScale NOTIFY uiScaleChanged)
+    Q_PROPERTY(bool darkMode READ darkMode WRITE setDarkMode NOTIFY darkModeChanged)
+    Q_PROPERTY(bool showDeviceSetup READ showDeviceSetup WRITE setShowDeviceSetup NOTIFY
+                   showDeviceSetupChanged)
+    Q_PROPERTY(bool showWarnings READ showWarnings WRITE setShowWarnings NOTIFY
+                   showWarningsChanged)
+    Q_PROPERTY(bool noUpdater READ noUpdater CONSTANT)
+    Q_PROPERTY(bool psiBuild READ psiBuild CONSTANT)
+
+   public:
+    explicit VirtualStudio(bool firstRun = false, QObject* parent = nullptr);
+    ~VirtualStudio() override;
+
+    void setStandardWindow(QSharedPointer<QJackTrip> window);
+    void show();
+
+    bool showFirstRun();
+    bool hasRefreshToken();
+    QString versionString();
+    QString logoSection();
+    bool selectableBackend();
+    QString audioBackend();
+    void setAudioBackend(const QString& backend);
+    int inputDevice();
+    void setInputDevice(int device);
+    int outputDevice();
+    void setOutputDevice(int device);
+    int bufferSize();
+    void setBufferSize(int index);
+    int currentStudio();
+    QString connectionState();
+    QString updateChannel();
+    void setUpdateChannel(const QString& channel);
+    bool showInactive();
+    void setShowInactive(bool inactive);
+    bool showSelfHosted();
+    void setShowSelfHosted(bool selfHosted);
+    float fontScale();
+    float uiScale();
+    void setUiScale(float scale);
+    bool darkMode();
+    void setDarkMode(bool dark);
+    bool showDeviceSetup();
+    void setShowDeviceSetup(bool show);
+    bool showWarnings();
+    void setShowWarnings(bool show);
+    bool noUpdater();
+    bool psiBuild();
+
+   public slots:
+    void toStandard();
+    void toVirtualStudio();
+    void login();
+    void logout();
+    void refreshStudios(int index);
+    void refreshDevices();
+    void revertSettings();
+    void applySettings();
+    void connectToStudio(int studioIndex);
+    void completeConnection();
+    void disconnect();
+    void manageStudio(int studioIndex);
+    void createStudio();
+    void showAbout();
+    void exit();
+
+   signals:
+    void authSucceeded();
+    void authFailed();
+    void connected();
+    void disconnected();
+    void refreshFinished(int index);
+    void showFirstRunChanged();
+    void hasRefreshTokenChanged();
+    void logoSectionChanged();
+    void audioBackendChanged();
+    void inputDeviceChanged();
+    void outputDeviceChanged();
+    void bufferSizeChanged();
+    void currentStudioChanged();
+    void showInactiveChanged();
+    void showSelfHostedChanged();
+    void connectionStateChanged();
+    void updateChannelChanged();
+    void showDeviceSetupChanged();
+    void showWarningsChanged();
+    void uiScaleChanged();
+    void newScale();
+    void darkModeChanged();
+    void signalExit();
+    void periodicRefresh();
+
+   private slots:
+    void slotAuthSucceded();
+    void slotAuthFailed();
+    void processFinished();
+    void processError(const QString& errorMessage);
+    void receivedConnectionFromPeer();
+    void checkForHostname();
+    void endRetryPeriod();
+    void launchBrowser(const QUrl& url);
+
+   private:
+    void setupAuthenticator();
+    void getServerList(bool firstLoad = false, int index = -1);
+    void getUserId();
+    void getSubscriptions();
+#ifdef RT_AUDIO
+    void getDeviceList(QStringList* list, bool isInput);
+#endif
+    void stopStudio();
+
+    bool m_showFirstRun = false;
+    bool m_checkSsl     = true;
+    QString m_updateChannel;
+    QString m_refreshToken;
+    QString m_userId;
+    VsQuickView m_view;
+    QSharedPointer<QJackTrip> m_standardWindow;
+    QScopedPointer<QOAuth2AuthorizationCodeFlow> m_authenticator;
+
+    QList<QObject*> m_servers;
+    QStringList m_subscribedServers;
+    QString m_logoSection     = QStringLiteral("Your Studios");
+    bool m_selectableBackend  = true;
+    bool m_useRtAudio         = false;
+    int m_currentStudio       = -1;
+    QString m_connectionState = QStringLiteral("Connecting...");
+    QScopedPointer<JackTrip> m_jackTrip;
+    QTimer m_startTimer;
+    QTimer m_retryPeriodTimer;
+    bool m_startedStudio = false;
+    bool m_retryPeriod;
+    bool m_jackTripRunning = false;
+
+    QTimer m_refreshTimer;
+    QMutex m_refreshMutex;
+    bool m_allowRefresh      = true;
+    bool m_refreshInProgress = false;
+
+    bool m_onConnectedScreen = false;
+    bool m_isExiting         = false;
+    bool m_showInactive      = false;
+    bool m_showSelfHosted    = false;
+    bool m_showDeviceSetup   = true;
+    bool m_showWarnings      = true;
+    float m_fontScale        = 1;
+    float m_uiScale;
+    float m_previousUiScale;
+    bool m_darkMode = false;
+
+#ifdef RT_AUDIO
+    QStringList m_inputDeviceList;
+    QStringList m_outputDeviceList;
+    QString m_inputDevice;
+    QString m_outputDevice;
+    quint16 m_bufferSize;
+    QString m_previousInput;
+    QString m_previousOutput;
+    quint16 m_previousBuffer;
+    bool m_previousUseRtAudio = false;
+#endif
+    QStringList m_bufferOptions        = {"16", "32", "64", "128", "256", "512", "1024"};
+    QStringList m_updateChannelOptions = {"Stable", "Edge"};
+
+#ifdef __APPLE__
+    NoNap m_noNap;
+#endif
+};
+
+#endif  // VIRTUALSTUDIO_H
diff --git a/src/gui/vs.qml b/src/gui/vs.qml
new file mode 100644 (file)
index 0000000..1465189
--- /dev/null
@@ -0,0 +1,122 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+
+Rectangle {
+    property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    
+    width: 696
+    height: 577
+    color: backgroundColour
+    state: virtualstudio.showFirstRun ? "start" : "login"
+    anchors.fill: parent
+
+    id: window
+    states: [
+        State {
+            name: "start"
+            PropertyChanges { target: startScreen; x: 0 }
+            PropertyChanges { target: loginScreen; x: window.width; failTextVisible: loginScreen.failTextVisible }
+            PropertyChanges { target: setupScreen; x: window.width }
+            PropertyChanges { target: browseScreen; x: window.width }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: connectedScreen; x: window.width }
+        },
+
+        State {
+            name: "login"
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: loginScreen; x: 0; failTextVisible: false }
+            PropertyChanges { target: setupScreen; x: window.width }
+            PropertyChanges { target: browseScreen; x: window.width }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: connectedScreen; x: window.width }
+        },
+
+        State {
+            name: "setup"
+            PropertyChanges { target: loginScreen; x: -loginScreen.width }
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: setupScreen; x: 0 }
+            PropertyChanges { target: browseScreen; x: window.width }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: connectedScreen; x: window.width }
+        },
+
+        State {
+            name: "browse"
+            PropertyChanges { target: loginScreen; x: -loginScreen.width }
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: setupScreen; x: -setupScreen.width }
+            PropertyChanges { target: browseScreen; x: 0 }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: connectedScreen; x: window.width }
+        },
+
+        State {
+            name: "settings"
+            PropertyChanges { target: loginScreen; x: -loginScreen.width }
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: setupScreen; x: -setupScreen.width }
+            PropertyChanges { target: browseScreen; x: -browseScreen.width }
+            PropertyChanges { target: settingsScreen; x: 0 }
+            PropertyChanges { target: connectedScreen; x: window.width }
+        },
+
+        State {
+            name: "connected"
+            PropertyChanges { target: loginScreen; x: -loginScreen.width }
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: setupScreen; x: -setupScreen.width }
+            PropertyChanges { target: browseScreen; x: -browseScreen.width }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: connectedScreen; x: 0 }
+        }
+    ]
+
+    transitions: Transition {
+        NumberAnimation { properties: "x"; duration: 800; easing.type: Easing.InOutQuad }
+    }
+
+    FirstLaunch {
+        id: startScreen
+    }
+    
+    Setup {
+        id: setupScreen
+    }
+
+    Browse {
+        id: browseScreen
+    }
+    
+    Login {
+        id: loginScreen
+    }
+
+    Settings {
+        id: settingsScreen
+    }
+
+    Connected {
+        id: connectedScreen
+    }
+
+    Connections {
+        target: virtualstudio
+        onAuthSucceeded: { 
+            if (virtualstudio.showDeviceSetup) {
+                window.state = "setup";
+            } else {
+                window.state = "browse";
+            }
+        }
+        onAuthFailed: {
+            loginScreen.failTextVisible = true;
+        }
+        // onConnected: { }
+        onDisconnected: {
+            window.state = "browse";
+        }
+    }
+}
diff --git a/src/gui/vsQuickView.cpp b/src/gui/vsQuickView.cpp
new file mode 100644 (file)
index 0000000..791104f
--- /dev/null
@@ -0,0 +1,47 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  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.
+*/
+//*****************************************************************
+
+/**
+ * \file vsQuickView.cpp
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#include "vsQuickView.h"
+
+bool VsQuickView::event(QEvent* event)
+{
+    if (event->type() == QEvent::Close) {
+        emit windowClose();
+        event->ignore();
+    }
+    return QQuickView::event(event);
+}
diff --git a/src/gui/vsQuickView.h b/src/gui/vsQuickView.h
new file mode 100644 (file)
index 0000000..bb9ad78
--- /dev/null
@@ -0,0 +1,55 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  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.
+*/
+//*****************************************************************
+
+/**
+ * \file vsQuickView.h
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#ifndef VSQUICKVIEW_H
+#define VSQUICKVIEW_H
+
+#include <QQuickView>
+
+class VsQuickView : public QQuickView
+{
+    Q_OBJECT
+
+   public:
+    VsQuickView(QWindow* parent = nullptr) : QQuickView(parent) {}
+    bool event(QEvent* event) override;
+
+   signals:
+    void windowClose();
+};
+
+#endif  // VSQUICKVIEW_H
diff --git a/src/gui/vsServerInfo.cpp b/src/gui/vsServerInfo.cpp
new file mode 100644 (file)
index 0000000..ec9a761
--- /dev/null
@@ -0,0 +1,211 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  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.
+*/
+//*****************************************************************
+
+/**
+ * \file vsServerInfo.cpp
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#include "vsServerInfo.h"
+
+VsServerInfo::VsServerInfo(QObject* parent) : QObject(parent) {}
+
+VsServerInfo::serverSectionT VsServerInfo::section()
+{
+    return m_section;
+}
+
+QString VsServerInfo::type()
+{
+    if (m_section == YOUR_STUDIOS) {
+        return QStringLiteral("Your Studios");
+    } else if (m_section == SUBSCRIBED_STUDIOS) {
+        return QStringLiteral("Subscribed Studios");
+    } else {
+        return QStringLiteral("Public Studios");
+    }
+}
+
+void VsServerInfo::setSection(serverSectionT section)
+{
+    m_section = section;
+}
+
+QString VsServerInfo::name()
+{
+    return m_name;
+}
+
+void VsServerInfo::setName(const QString& name)
+{
+    m_name = name;
+}
+
+QString VsServerInfo::host()
+{
+    return m_host;
+}
+
+QString VsServerInfo::status()
+{
+    return m_status;
+}
+
+bool VsServerInfo::canConnect()
+{
+    return !m_host.isEmpty() && m_status == "Ready";
+}
+
+bool VsServerInfo::canStart()
+{
+#ifdef PSI
+    return true;
+#else
+    return false;
+#endif
+}
+
+void VsServerInfo::setHost(const QString& host)
+{
+    m_host = host;
+    emit canConnectChanged();
+}
+
+void VsServerInfo::setStatus(const QString& status)
+{
+    m_status = status;
+    emit canConnectChanged();
+}
+
+quint16 VsServerInfo::port()
+{
+    return m_port;
+}
+
+void VsServerInfo::setPort(quint16 port)
+{
+    m_port = port;
+}
+
+bool VsServerInfo::isPublic()
+{
+    return m_isPublic;
+}
+
+void VsServerInfo::setIsPublic(bool isPublic)
+{
+    m_isPublic = isPublic;
+}
+
+QString VsServerInfo::region()
+{
+    return m_region;
+}
+
+QString VsServerInfo::flag()
+{
+    QStringList parts = m_region.split(QStringLiteral("-"));
+    if (parts.count() > 1) {
+        QString countryCode = parts.at(1).toUpper();
+        if (countryCode == QStringLiteral("TF")) {
+            countryCode = QStringLiteral("TW");
+        }
+        return QStringLiteral("flags/%1.svg").arg(countryCode);
+    }
+    // Have a fallback here
+    return QStringLiteral("flags/US.svg");
+}
+
+QString VsServerInfo::location()
+{
+    if (m_region.split(QStringLiteral("-")).count() > 2) {
+        return m_region.section(QStringLiteral("-"), 2);
+    }
+    return m_region;
+}
+
+void VsServerInfo::setRegion(const QString& region)
+{
+    m_region = region;
+}
+
+bool VsServerInfo::isManageable()
+{
+    return m_isManageable;
+}
+
+void VsServerInfo::setIsManageable(bool isManageable)
+{
+    m_isManageable = isManageable;
+}
+
+quint16 VsServerInfo::period()
+{
+    return m_period;
+}
+
+void VsServerInfo::setPeriod(quint16 period)
+{
+    m_period = period;
+}
+
+quint32 VsServerInfo::sampleRate()
+{
+    return m_sampleRate;
+}
+
+void VsServerInfo::setSampleRate(quint32 sampleRate)
+{
+    m_sampleRate = sampleRate;
+}
+
+quint16 VsServerInfo::queueBuffer()
+{
+    return m_queueBuffer;
+}
+
+void VsServerInfo::setQueueBuffer(quint16 queueBuffer)
+{
+    m_queueBuffer = queueBuffer;
+}
+
+QString VsServerInfo::id()
+{
+    return m_id;
+}
+
+void VsServerInfo::setId(const QString& id)
+{
+    m_id = id;
+}
+
+VsServerInfo::~VsServerInfo() = default;
diff --git a/src/gui/vsServerInfo.h b/src/gui/vsServerInfo.h
new file mode 100644 (file)
index 0000000..fecb849
--- /dev/null
@@ -0,0 +1,137 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  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.
+*/
+//*****************************************************************
+
+/**
+ * \file vsServerInfo.h
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#ifndef VSSERVERINFO_H
+#define VSSERVERINFO_H
+
+#include <QObject>
+
+class VsServerInfo : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QString type READ type CONSTANT)
+    Q_PROPERTY(QString name READ name CONSTANT)
+    // Q_PROPERTY(QString host READ host CONSTANT)
+    Q_PROPERTY(bool canConnect READ canConnect NOTIFY canConnectChanged)
+    Q_PROPERTY(bool canStart READ canStart CONSTANT)
+    // Q_PROPERTY(quint16 port READ port CONSTANT)
+    Q_PROPERTY(bool isPublic READ isPublic CONSTANT)
+    Q_PROPERTY(QString flag READ flag CONSTANT)
+    Q_PROPERTY(QString location READ location CONSTANT)
+    Q_PROPERTY(bool isManageable READ isManageable CONSTANT)
+    Q_PROPERTY(quint16 period READ period CONSTANT)
+    Q_PROPERTY(quint32 sampleRate READ sampleRate CONSTANT)
+    Q_PROPERTY(quint16 queueBuffer READ queueBuffer CONSTANT)
+    Q_PROPERTY(QString status READ status CONSTANT)
+
+   public:
+    enum serverSectionT { YOUR_STUDIOS, SUBSCRIBED_STUDIOS, PUBLIC_STUDIOS };
+
+    explicit VsServerInfo(QObject* parent = nullptr);
+    ~VsServerInfo() override;
+
+    serverSectionT section();
+    QString type();
+    void setSection(serverSectionT section);
+    QString name();
+    void setName(const QString& name);
+    QString host();
+    bool canConnect();
+    bool canStart();
+    void setHost(const QString& host);
+    quint16 port();
+    void setPort(quint16 port);
+    bool isPublic();
+    void setIsPublic(bool isPublic);
+    QString region();
+    QString flag();
+    QString location();
+    void setRegion(const QString& region);
+    bool isManageable();
+    void setIsManageable(bool isManageable);
+    quint16 period();
+    void setPeriod(quint16 period);
+    quint32 sampleRate();
+    void setSampleRate(quint32 sampleRate);
+    quint16 queueBuffer();
+    void setQueueBuffer(quint16 queueBuffer);
+    QString id();
+    void setId(const QString& id);
+    QString status();
+    void setStatus(const QString& status);
+
+   signals:
+    void canConnectChanged();
+
+   private:
+    serverSectionT m_section = PUBLIC_STUDIOS;
+    QString m_name;
+    QString m_host;
+    quint16 m_port;
+    bool m_isPublic;
+    QString m_region;
+    bool m_isManageable;
+    quint16 m_period;
+    quint32 m_sampleRate;
+    quint16 m_queueBuffer;
+    QString m_id;
+    QString m_status;
+
+    /* Remaining JSON fields
+    "loopback": true,
+    "stereo": true,
+    "type": "JackTrip",
+    "managed": true,
+    "size": "c5.large",
+    "mixBranch": "main",
+    "mixCode": "SimpleMix(~maxClients).masterVolume_(1).connect.start;",
+    "enabled": true,
+    "admin": true,
+    "cloudId": "string",
+    "owner": true,
+    "ownerId": "string",
+    "status": "Ready",
+    "sessionId": "1636042722abcdefg",
+    "subStatus": "Active",
+    "createdAt": "2021-09-07T17:15:38Z",
+    "expiresAt": "2021-09-07T17:15:38Z",
+    "updatedAt": "2021-09-07T17:15:38Z"
+    */
+};
+
+#endif  // VSSERVERINFO_H
diff --git a/src/gui/wedge.svg b/src/gui/wedge.svg
new file mode 100644 (file)
index 0000000..230cdd2
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="13.758333mm"
+   height="21.960417mm"
+   viewBox="0 0 13.758333 21.960417"
+   version="1.1"
+   id="svg5"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   sodipodi:docname="wedge.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview7"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:document-units="mm"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:zoom="2.0630341"
+     inkscape:cx="-46.291044"
+     inkscape:cy="47.26049"
+     inkscape:window-width="1920"
+     inkscape:window-height="1007"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1" />
+  <defs
+     id="defs2" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <path
+       style="fill:#0c1424;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 0,0 h 13.758333 l -2.38125,21.960417 H 0 Z"
+       id="wedge"
+       sodipodi:nodetypes="ccccc" />
+  </g>
+</svg>
diff --git a/src/gui/wedge_inactive.svg b/src/gui/wedge_inactive.svg
new file mode 100644 (file)
index 0000000..68ecbcf
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="13.758333mm"
+   height="21.960417mm"
+   viewBox="0 0 13.758333 21.960417"
+   version="1.1"
+   id="svg5"
+   inkscape:version="1.1.2 (b8e25be8, 2022-02-05)"
+   sodipodi:docname="wedge_inactive.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview7"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:document-units="mm"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:zoom="4.0967594"
+     inkscape:cx="0.36614306"
+     inkscape:cy="41.008022"
+     inkscape:window-width="1312"
+     inkscape:window-height="856"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="layer1" />
+  <defs
+     id="defs2" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <path
+       style="fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 0,0 h 13.758333 l -2.38125,21.960417 H 0 Z"
+       id="wedge"
+       sodipodi:nodetypes="ccccc" />
+  </g>
+</svg>
index b71be8120c724b1ce091347925968f7d14471f1d..c20c196a9169a87653fd05bbe7fcbfe8868a484b 100644 (file)
@@ -40,7 +40,7 @@
 
 #include "AudioInterface.h"
 
-constexpr const char* const gVersion = "1.5.3";  ///< JackTrip version
+constexpr const char* const gVersion = "1.6.1";  ///< JackTrip version
 
 //*******************************************************************************
 /// \name Default Values
index 6ff79097d3a10de3bcd8e0aabe10f78ae2eed99b..ebe553b14f8c4c9b1d0a86f1b6da16c4ccc992ab 100644 (file)
 #include <QApplication>
 #include <QCommandLineParser>
 
+#ifndef NO_UPDATER
+#include "dblsqd/feed.h"
+#include "dblsqd/update_dialog.h"
+#endif
+
+#ifndef NO_VS
+#include <QQuickView>
+#include <QSettings>
+
+#include "gui/virtualstudio.h"
+#endif
+
 #include "gui/qjacktrip.h"
 #else
 #include <QCoreApplication>
@@ -109,6 +121,8 @@ QCoreApplication* createApplication(int& argc, char* argv[])
             std::exit(1);
         }
 #endif
+        // Turn on high DPI support.
+        QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
         return new QApplication(argc, argv);
 #endif  // NO_GUI
     } else {
@@ -199,6 +213,9 @@ bool isRunFromCmd()
             if (size >= 7 && strncmp(pname + size - 7, "cmd.exe", 7) == 0) {
                 return true;
             }
+            if (size >= 6 && strncmp(pname + size - 6, "wt.exe", 6) == 0) {
+                return true;
+            }
         } else {
             CloseHandle(h);
         }
@@ -214,7 +231,12 @@ int main(int argc, char* argv[])
     QScopedPointer<JackTrip> jackTrip;
     QScopedPointer<UdpHubListener> udpHub;
 #ifndef NO_GUI
-    QScopedPointer<QJackTrip> window;
+    QSharedPointer<QJackTrip> window;
+
+#ifndef NO_VS
+    QSharedPointer<VirtualStudio> vs;
+#endif
+
     if (qobject_cast<QApplication*>(app.data())) {
         // Start the GUI if there are no command line options.
 #ifdef _WIN32
@@ -224,7 +246,10 @@ int main(int argc, char* argv[])
             FreeConsole();
         }
 #endif  // _WIN32
-        app->setApplicationName(QStringLiteral("QJackTrip"));
+        app->setOrganizationName(QStringLiteral("jacktrip"));
+        app->setOrganizationDomain(QStringLiteral("jacktrip.org"));
+        app->setApplicationName(QStringLiteral("JackTrip"));
+        app->setApplicationVersion(gVersion);
 
         QCommandLineParser parser;
         QCommandLineOption verboseOption(QStringList() << QStringLiteral("V")
@@ -235,10 +260,55 @@ int main(int argc, char* argv[])
             gVerboseFlag = true;
         }
 
+#ifndef NO_VS
+        // Check if we need to show our first run window.
+        QSettings settings;
+        int uiMode = settings.value(QStringLiteral("UiMode"), QJackTrip::UNSET).toInt();
+        QString updateChannel = settings.value(QStringLiteral("UpdateChannel"), "stable")
+                                    .toString()
+                                    .toLower();
+#endif  // NO_VS
         window.reset(new QJackTrip(argc));
         QObject::connect(window.data(), &QJackTrip::signalExit, app.data(),
                          &QCoreApplication::quit, Qt::QueuedConnection);
+#ifndef NO_VS
+        vs.reset(new VirtualStudio(uiMode == QJackTrip::UNSET));
+        QObject::connect(vs.data(), &VirtualStudio::signalExit, app.data(),
+                         &QCoreApplication::quit, Qt::QueuedConnection);
+        vs->setStandardWindow(window);
+        window->setVs(vs);
+
+        if (uiMode == QJackTrip::UNSET) {
+            vs->show();
+        } else if (uiMode == QJackTrip::VIRTUAL_STUDIO) {
+            vs->show();
+        } else {
+            window->show();
+        }
+#else
         window->show();
+#endif  // NO_VS
+
+#ifndef NO_UPDATER
+        // Setup auto-update feed
+        dblsqd::Feed* feed = 0;
+        QString baseUrl =
+            "https://raw.githubusercontent.com/jacktrip/jacktrip/dev/releases";
+#ifdef Q_OS_WIN
+        feed = new dblsqd::Feed();
+        feed->setUrl(
+            QUrl(QString("%1/%2/%3-manifests.json").arg(baseUrl, updateChannel, "win")));
+#endif
+#ifdef Q_OS_MACOS
+        feed = new dblsqd::Feed();
+        feed->setUrl(
+            QUrl(QString("%1/%2/%3-manifests.json").arg(baseUrl, updateChannel, "mac")));
+#endif
+        if (feed) {
+            dblsqd::UpdateDialog* updateDialog = new dblsqd::UpdateDialog(feed);
+            updateDialog->setIcon(":/qjacktrip/icon.png");
+        }
+#endif  // NO_UPDATER
     } else {
 #endif  // NO_GUI
         // Otherwise use the non-GUI version, and parse our command line.
diff --git a/win/CodeSignTool/CodeSignTool.sh b/win/CodeSignTool/CodeSignTool.sh
new file mode 100755 (executable)
index 0000000..10c4bc9
--- /dev/null
@@ -0,0 +1 @@
+java -cp "./jar/picocli-4.6.1.jar:./jar/bcprov-jdk15on-1.65.01.jar:./jar/httpclient-4.5.13.jar:./jar/json-simple-1.1.1.jar:./jar/jsign-core-3.1.jar:./jar/commons-io-2.8.0.jar:./jar/bcpkix-jdk15on-1.65.jar:./jar/code_sign_tool-1.2.2.jar:./jar/httpcore-4.4.13.jar:./jar/commons-logging-1.2.jar:./jar/log4j-api-2.17.1.jar:./jar/log4j-core-2.17.1.jar:./jar/poi-4.1.2.jar:./jar/commons-lang3-3.9.jar:./jar/commons-math3-3.6.1.jar:./jar/totp-1.0.jar:./jar/commons-codec-1.15.jar" com.ssl.code.signing.tool.CodeSignTool $@
diff --git a/win/CodeSignTool/conf/code_sign_tool.properties b/win/CodeSignTool/conf/code_sign_tool.properties
new file mode 100644 (file)
index 0000000..5622896
--- /dev/null
@@ -0,0 +1,4 @@
+CLIENT_ID=kaXTRACNijSWsFdRKg_KAfD3fqrBlzMbWs6TwWHwAn8\r
+OAUTH2_ENDPOINT=https://login.ssl.com/oauth2/token\r
+CSC_API_ENDPOINT=https://cs.ssl.com\r
+TSA_URL=http://ts.ssl.com
\ No newline at end of file
diff --git a/win/CodeSignTool/conf/log4j2.xml b/win/CodeSignTool/conf/log4j2.xml
new file mode 100644 (file)
index 0000000..e3ee419
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<Configuration status="WARN">\r
+    <Properties>\r
+        <Property name="LOG_PATTERN">\r
+            %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n\r
+        </Property>\r
+    </Properties>\r
+    <Appenders>\r
+        <RollingFile name="FileAppenderApp" fileName="./logs/code_signing_tool.log"\r
+                     filePattern="./logs/code_signing_tool-%d{yyyy-MM-dd}.log">\r
+            <PatternLayout>\r
+                <Pattern>${LOG_PATTERN}</Pattern>\r
+            </PatternLayout>\r
+            <Policies>\r
+                <TimeBasedTriggeringPolicy/>\r
+            </Policies>\r
+            <DefaultRolloverStrategy fileIndex="nomax"/>\r
+        </RollingFile>\r
+    </Appenders>\r
+    <Loggers>\r
+        <Root level="info">\r
+            <AppenderRef ref="FileAppenderApp" />\r
+        </Root>\r
+        <!--<Logger name="com.ssl.code.signing.tool" level="info" additivity="false">\r
+            <AppenderRef ref="FileAppenderApp" />\r
+        </Logger>-->\r
+    </Loggers>\r
+</Configuration>
\ No newline at end of file
index 80f5a1411ed9d0f11f94a0707a2c339bbed2cd0c..71a53402fe49876bb5e67dfb61255ad7cf4fd3cd 100755 (executable)
@@ -44,13 +44,19 @@ set "WIXDEFINES="
 for /f "tokens=*" %%a in ('%QTLIBPATH%\objdump -p jacktrip.exe ^| findstr Qt5Core.dll') do set DYNAMIC_QT=%%a\r
 if defined DYNAMIC_QT (\r
        echo Including Qt Files\r
-       %QTBINPATH%\windeployqt jacktrip.exe\r
+       for /f "tokens=*" %%a in ('%QTLIBPATH%\objdump -p jacktrip.exe ^| findstr Qt5Qml.dll') do set VS=%%a\r
+       if defined VS (\r
+               %QTBINPATH%\windeployqt --qmldir ..\..\src\gui jacktrip.exe\r
+               set WIXDEFINES=%WIXDEFINES% -dvs\r
+       ) else (\r
+               %QTBINPATH%\windeployqt jacktrip.exe\r
+       )\r
        copy "%QTLIBPATH%\libgcc_s_seh-1.dll" .\\r
        copy "%QTLIBPATH%\libstdc++-6.dll" .\\r
        copy "%QTLIBPATH%\libwinpthread-1.dll" .\\r
        copy "%SSLPATH%\libcrypto-1_1-x64.dll" .\\r
        copy "%SSLPATH%\libssl-1_1-x64.dll" .\\r
-       set WIXDEFINES=%WIXDEFINES% -ddynamic\r
+       set WIXDEFINES=!WIXDEFINES! -ddynamic\r
 )\r
 for /f "tokens=*" %%a in ('%QTLIBPATH%\objdump -p jacktrip.exe ^| findstr librtaudio.dll') do set RTAUDIO=%%a\r
 if defined RTAUDIO (\r
@@ -70,6 +76,6 @@ rem Get our version number
 for /f "tokens=*" %%a in ('.\jacktrip -v ^| findstr VERSION') do for %%b in (%%~a) do set VERSION=%%b\r
 for /f "tokens=1 delims=-" %%a in ("%VERSION%") do set VERSION=%%a\r
 echo Version=%VERSION%\r
-candle.exe -dVersion=%VERSION%%WIXDEFINES% jacktrip.wxs files.wxs\r
-light.exe -ext WixUIExtension -o JackTrip.msi jacktrip.wixobj files.wixobj\r
+candle.exe -ext WixUIExtension -ext WixUtilExtension -dVersion=%VERSION%%WIXDEFINES% jacktrip.wxs files.wxs\r
+light.exe -ext WixUIExtension -ext WixUtilExtension -o JackTrip.msi jacktrip.wixobj files.wixobj\r
 endlocal\r
index 94c3a3c95f4d356a3f7b1ecf1ee5cea4283df6c2..00e2c1e5a22072f7dfc14ac5230e0e4751f48931 100644 (file)
                 <Component Id="cmpF5EA7E37332E95C614BD2D7EAA933E5D" Guid="6516FDA0-5035-420D-AADF-41D1E3735EDA">\r
                     <File Id="fil49E3CA421049811064612E40502A6147" KeyPath="yes" Source="SourceDir\translations\qt_sk.qm" />\r
                 </Component>\r
+                <Component Id="cmp8DAF13208A863A841AEE7A46E79FB87C" Guid="{A628AE90-BA9E-4CB9-86E0-02E6F2A0358B}">\r
+                    <File Id="filD139779D4ABC7CD76AE12C511550A8C3" KeyPath="yes" Source="SourceDir\translations\qt_tr.qm" />\r
+                </Component>\r
                 <Component Id="cmpA214B663A372012F5DCBA6A5CDAE421D" Guid="790AF12A-1970-4B96-BFAD-925734F604EB">\r
                     <File Id="fil1380C741E2DBC1F60AA4DC32D66B1E6C" KeyPath="yes" Source="SourceDir\translations\qt_uk.qm" />\r
                 </Component>\r
                     <File Id="fil9085627E4E33630ABCDB62A5CC05B794" KeyPath="yes" Source="SourceDir\translations\qt_zh_TW.qm" />\r
                 </Component>\r
             </Directory>\r
+<?endif?>\r
+<?ifdef vs?>\r
+            <Component Id="cmp26187F331810660B2B23AB9501BE69AF" Guid="{B9D40CBB-9764-4DAA-9121-B74B8EADE326}">\r
+                <File Id="fil548462476B423A98BF378EB6B7FED697" KeyPath="yes" Source="SourceDir\Qt5NetworkAuth.dll" />\r
+            </Component>\r
+            <Component Id="cmp10C81658E3C8F63B19EF9D1C26B58363" Guid="{67FF3D4C-A3F5-4C19-8988-CB8111149469}">\r
+                <File Id="fil0F208DFAC8D7EB86E7793690C6ECF15E" KeyPath="yes" Source="SourceDir\Qt5Qml.dll" />\r
+            </Component>\r
+            <Component Id="cmp5B4749C387D33EAF993DDC89F9DBC102" Guid="{E1A0612E-1BAA-4304-A53F-04F9E5FC2C95}">\r
+                <File Id="fil4D31B1A954CDE740FB32D9FFC142CA37" KeyPath="yes" Source="SourceDir\Qt5QmlModels.dll" />\r
+            </Component>\r
+            <Component Id="cmp7F83581B3F7835094D371708381AD391" Guid="{F123314E-1319-4B12-90E9-BB27786C2234}">\r
+                <File Id="filEFD45339E9495CECA6AD117B583ED589" KeyPath="yes" Source="SourceDir\Qt5QmlWorkerScript.dll" />\r
+            </Component>\r
+            <Component Id="cmp33B78CE22F0CBCA4F41C5037A792210C" Guid="{5653E0BB-A956-4565-B2AD-11712CC59571}">\r
+                <File Id="fil87DAAA273949BD9AD08743E49398302F" KeyPath="yes" Source="SourceDir\Qt5Quick.dll" />\r
+            </Component>\r
+            <Component Id="cmpB5571DB4C3153CA848ED1238DBCD4281" Guid="{AAD89C99-7211-4F02-B07D-A89CD18DF9D5}">\r
+                <File Id="fil07F4E15C2DC44A3DA783DB682CA4FF5C" KeyPath="yes" Source="SourceDir\Qt5QuickControls2.dll" />\r
+            </Component>\r
+            <Component Id="cmp4C1BE3EACCCE195C8B88434D5DF77DE5" Guid="{C8585769-1336-4CC6-B8CD-5D9CEE77F079}">\r
+                <File Id="filBE9D9480511F5B210D1FE72753A138A6" KeyPath="yes" Source="SourceDir\Qt5QuickTemplates2.dll" />\r
+            </Component>\r
+            <Component Id="cmpE4BE73E4CCE2EE3CCA22CAE0C82CD298" Guid="{33CEA31D-C927-4EA2-B6ED-BE88D77154C3}">\r
+                <File Id="fil0A02B63810F4F84B87ED6449067762AA" KeyPath="yes" Source="SourceDir\Qt5RemoteObjects.dll" />\r
+            </Component>\r
+            <Directory Id="dirAB1B0C3E5160B20B93DD2AC0AE9E5509" Name="qmltooling">\r
+                <Component Id="cmpFEC2C3E75BBEACDB1A6F4AFB808CD58E" Guid="{64ED0E30-E431-4238-BD39-465D306D9520}">\r
+                    <File Id="fil09C44C978E58830A831DCC0DBAF3AEA2" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_debugger.dll" />\r
+                </Component>\r
+                <Component Id="cmp8B3D94901F4D369CD276B339B8B00F70" Guid="{554EAB57-00E2-4FCB-87FF-6F07EF14EDC4}">\r
+                    <File Id="fil3E82AB436354230BADBA9DD68B38C2AD" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_inspector.dll" />\r
+                </Component>\r
+                <Component Id="cmp0B6C2EAABA7EAA14C510F4C9D68A3F40" Guid="{A211CD9B-6E6A-4600-9FA1-8DF999A942AE}">\r
+                    <File Id="fil8CD8981EA2309B09460020F4D9AC275C" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_local.dll" />\r
+                </Component>\r
+                <Component Id="cmpE75E4FEE65DCA51D044DF4B2AEDCD828" Guid="{625FA227-EB72-4C9E-AD73-8FAC837EE2D2}">\r
+                    <File Id="fil2A36EBC77D39FBFF748DE570C1F7DC96" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_messages.dll" />\r
+                </Component>\r
+                <Component Id="cmpA8BBE7010AD624EFBA7166925167402F" Guid="{D044FD19-6C91-4736-80E4-D2DA97CD60B4}">\r
+                    <File Id="filA4CF6CC4639F20D9F436C3DE2C32102E" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_native.dll" />\r
+                </Component>\r
+                <Component Id="cmp00B1DB73ED33EC1DD6658036D3901CB7" Guid="{0FCBA1AB-A703-4399-A518-94B72B55BBCF}">\r
+                    <File Id="fil04B1E67A26C29E877D34C6CDB21D302D" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_nativedebugger.dll" />\r
+                </Component>\r
+                <Component Id="cmp9F481CFBAECB8853AF07C1D5060B77D7" Guid="{C2D900C8-5DE0-4EFA-A124-A3865FFE4149}">\r
+                    <File Id="filCD389466FEC0CB778B4940AD0354D4A0" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_preview.dll" />\r
+                </Component>\r
+                <Component Id="cmp93816105EAC51740B98AF9C1041D28D1" Guid="{D8F9A3EE-B929-4418-A3EE-F3228B05A5F2}">\r
+                    <File Id="filE23CD7951728F9C858C61FDBAFD405D2" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_profiler.dll" />\r
+                </Component>\r
+                <Component Id="cmp5ED50822278A4A846E891B41C07F7B6A" Guid="{FBDFDAD6-886A-479A-8ED1-D04B0028E9EA}">\r
+                    <File Id="fil935ED8A28D14AB20137795A9C347694D" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_quickprofiler.dll" />\r
+                </Component>\r
+                <Component Id="cmpACA0A17DA54991A194B823CE40D06263" Guid="{8AA14A55-6F27-48E4-9A18-90D618AD8194}">\r
+                    <File Id="fil6E61B7F24F3C3FDB83A90CEE46B2C831" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_server.dll" />\r
+                </Component>\r
+                <Component Id="cmp3D2C980EC975DBBCBD13A9178DD1A03B" Guid="{8AA853F3-4BAD-4E76-8F48-CB9FC961AE1F}">\r
+                    <File Id="fil0C06BD9370B146FE43DEFD060222FC12" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_tcp.dll" />\r
+                </Component>\r
+            </Directory>\r
+            <Directory Id="dir6932501040B63B39C33E640A395AEA58" Name="QtGraphicalEffects">\r
+                <Component Id="cmp356189FC3558D542AA8AD79067A42632" Guid="{25EE1F94-BDC5-4BF4-9CF5-4471CB2CDC99}">\r
+                    <File Id="filC50FC92D4690551FD9149804C9524CB6" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\Blend.qml" />\r
+                </Component>\r
+                <Component Id="cmpABC392DBCC9A5A469B77C129899873EE" Guid="{DB29E30C-5362-4A23-BB9D-C1E6B4569979}">\r
+                    <File Id="fil585EA7DCEFEA763B0D34D92C25BF236D" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\BrightnessContrast.qml" />\r
+                </Component>\r
+                <Component Id="cmp740F889D5799A2FF0F93A24058617EB6" Guid="{304F8096-0A76-4DA8-904E-DBC6ECB871EA}">\r
+                    <File Id="fil44DA8C781EB6783323659B96815E6B06" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\Colorize.qml" />\r
+                </Component>\r
+                <Component Id="cmp2799826256C0E84847E94E11C7878018" Guid="{9D087450-2C02-4859-8100-57FD4BE69464}">\r
+                    <File Id="fil79E5BBBAD1145AE67A9F57C761872AB7" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\ColorOverlay.qml" />\r
+                </Component>\r
+                <Component Id="cmp659424F663F96AE55FBA532B4CBB2EC8" Guid="{B9C2D28B-C4EF-45DB-B9BA-E1B9405D76F4}">\r
+                    <File Id="filACFDAF73B5B767483881AB3B6CD4F973" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\ConicalGradient.qml" />\r
+                </Component>\r
+                <Component Id="cmpDF6A99C1FA6B3943D690F21136BA7F9F" Guid="{03C439A4-BE19-49B8-9F1F-972FF89C415D}">\r
+                    <File Id="fil12DD1BF106F38168A5EE7CAFE7622E54" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\Desaturate.qml" />\r
+                </Component>\r
+                <Component Id="cmp81B459304782BB6AC65E381BA13726B0" Guid="{11D487DE-683F-4647-872C-C2E1DAA33042}">\r
+                    <File Id="fil71154C20C340083D34D22ED1F8EFA765" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\DirectionalBlur.qml" />\r
+                </Component>\r
+                <Component Id="cmp3CC0196A91D9C805D66BFFF4EA38EBE7" Guid="{16A372C2-116D-42AC-B431-3B220C37B9EE}">\r
+                    <File Id="fil0038BC20F6506BDA5417B08B88C92BC1" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\Displace.qml" />\r
+                </Component>\r
+                <Component Id="cmp67E65C02C04C3567636F47EFAA888D12" Guid="{2358043B-BBC7-450B-B5D3-8C26E5249358}">\r
+                    <File Id="fil7602377B1CFE07CFCA64BC753B0AE930" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\DropShadow.qml" />\r
+                </Component>\r
+                <Component Id="cmpADC8E894229BBB48BBC40D4F033FD791" Guid="{2EF6B35A-FD9D-42BA-A12C-3763684AC39C}">\r
+                    <File Id="filC46A98B519397B6CE253A5C9F8D5E0E4" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\FastBlur.qml" />\r
+                </Component>\r
+                <Component Id="cmp0D2D3E713559661141BC5DD9F687F29D" Guid="{7556A68C-05FC-44CB-B9FD-56535DE7653D}">\r
+                    <File Id="filA97688F2BA74A175F3E4EB3F9C1BB027" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\GammaAdjust.qml" />\r
+                </Component>\r
+                <Component Id="cmp3C102DD93EC83164DA87E7820364DDAC" Guid="{6C1D3B9D-A04C-4749-84B6-B276EDC28D20}">\r
+                    <File Id="fil5B2FC5A0BED021E222F6738C10D3081C" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\GaussianBlur.qml" />\r
+                </Component>\r
+                <Component Id="cmp4B666BB592E998D07B55C82CF76F5B1C" Guid="{FAC4A87D-2D90-46C6-98C6-D3A5CB94732C}">\r
+                    <File Id="filC5E145603A0E00BF0F22F2B8A67C9A22" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\Glow.qml" />\r
+                </Component>\r
+                <Component Id="cmp4C99F5024E504FEAB151291478878934" Guid="{E0EB60EE-815C-45C3-B5BD-F8D68E099B3B}">\r
+                    <File Id="fil5F059EE301BFBC9CC82E3CCBEDF542CE" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\HueSaturation.qml" />\r
+                </Component>\r
+                <Component Id="cmp84BC34E1E20AAE86B427C9CD3129C4FA" Guid="{0763338E-FBE7-40BC-9591-8F3B5DB576E3}">\r
+                    <File Id="filA56659AFD9BB9DFBFDDA906B787EFCDD" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\InnerShadow.qml" />\r
+                </Component>\r
+                <Component Id="cmpCBA14389AA89D290E481E0F2625854CE" Guid="{406FB202-E76B-4FFD-A1E4-8FEB2F67401F}">\r
+                    <File Id="fil0C15C4F0A2C4CBC263B38BB384C9DA59" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\LevelAdjust.qml" />\r
+                </Component>\r
+                <Component Id="cmp6FFF646F52B60CE854DA0A1B8C47EC55" Guid="{ED6F3223-0243-4539-96C3-8EE89715F16F}">\r
+                    <File Id="fil53C443677D967DDB997226A1954ED5C7" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\LinearGradient.qml" />\r
+                </Component>\r
+                <Component Id="cmp5E04D231070BE2CE70B2672E56C6B9DE" Guid="{04ACB86E-F6EE-4019-BB49-B556AAF08359}">\r
+                    <File Id="filA148939302F08F0A8B0CC79A709DFA15" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\MaskedBlur.qml" />\r
+                </Component>\r
+                <Component Id="cmp8C5CA067EC2AB41E7CAFE253F2EC86A8" Guid="{76E67D4A-0F5F-4AAF-8A78-847BF0EACDB4}">\r
+                    <File Id="fil2921C33FE8BBE6511E3EB7CA8C06115F" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\OpacityMask.qml" />\r
+                </Component>\r
+                <Component Id="cmp8441DAF2F2BC211F1F6FD10DCE7B51FA" Guid="{70CBBCF5-9030-4FE8-98F0-6665ABF0D3D7}">\r
+                    <File Id="filC8B8AEF14AC36FE600581D00FE128545" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\plugins.qmltypes" />\r
+                </Component>\r
+                <Component Id="cmpC3A1EC5089BA6B0798970A12E614BBDB" Guid="{3EC761B0-3826-451A-8D11-DD0B77E965DB}">\r
+                    <File Id="filDDA0AA71F77A367B9A901655165B1B51" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\qmldir" />\r
+                </Component>\r
+                <Component Id="cmp854B1173EACBB218B6AF849A7E8A56D7" Guid="{F879D731-522C-4E76-85F8-5DF0BD32421E}">\r
+                    <File Id="fil587B7775B793D6255A1BB1DE826E6A95" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\qtgraphicaleffectsplugin.dll" />\r
+                </Component>\r
+                <Component Id="cmp7D04EA58DD5BF794EB2D3A75609DEFF9" Guid="{2A98610D-386B-433E-ABC0-C878C1A632DA}">\r
+                    <File Id="fil9044BD6346C5BE02A746FAC271ED7427" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\RadialBlur.qml" />\r
+                </Component>\r
+                <Component Id="cmp688C34D162C3029BDE9A236F29106F11" Guid="{D34CDD08-E05A-4D45-9800-514F9C53AA0D}">\r
+                    <File Id="fil1B3F7EAA750FA63EF84F1FBA40ADEACB" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\RadialGradient.qml" />\r
+                </Component>\r
+                <Component Id="cmp7F9ABA6F59701379E7E54AB2EF4B01EA" Guid="{618C4845-14BB-41FB-8504-F256C0651929}">\r
+                    <File Id="fil89AD529AFFF6D62301719C7BAA8F28FD" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\RectangularGlow.qml" />\r
+                </Component>\r
+                <Component Id="cmp46E93DFCA823AE21F1FD874F13C0A6CA" Guid="{3248C193-B4CA-4AC1-8941-8096A52FE13F}">\r
+                    <File Id="filD9317E9BA692B66CC2C535F5B3A2F3BB" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\RecursiveBlur.qml" />\r
+                </Component>\r
+                <Component Id="cmp2F1F9DE69090BFD2946A7621F2DA410E" Guid="{2040A763-52C4-4BC3-8C8E-0B7F80C709A5}">\r
+                    <File Id="filBE37116DE14EE120D97D6725D27F4907" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\ThresholdMask.qml" />\r
+                </Component>\r
+                <Component Id="cmpE650E3AF80C0073C2B52BC369D45B79C" Guid="{59F7E175-5A48-4A83-9D2E-13A259A30660}">\r
+                    <File Id="fil4590ECDA88EBA09BB76C6180A1F4FA4B" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\ZoomBlur.qml" />\r
+                </Component>\r
+                <Directory Id="dir796AB3D358CFA364C82060B28476B244" Name="private">\r
+                    <Component Id="cmpB62C6E9BF562754AAC86BB52A361231D" Guid="{DD730ADB-7BC5-40E9-B34E-60DBB65391B6}">\r
+                        <File Id="filEC6B5D0DA92369DBBD0CF163C14FDFBD" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\DropShadowBase.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpE98977BCFBF844FDBCB5325BFC639084" Guid="{12EF9B8B-D9BC-41CB-97AF-5A8E73864194}">\r
+                        <File Id="filDA1AA9A2CBC27B220108E0997BC9F94C" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\DropShadowBase.qmlc" />\r
+                    </Component>\r
+                    <Component Id="cmpA11A92D9D7D10A9221DD05699B7B1B8C" Guid="{9CE273AB-3553-4CE8-9E0D-40202A50CEDC}">\r
+                        <File Id="fil746D3769B6A878D31F7B39B82F020A30" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\FastGlow.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpE9BBCB95E45E4F80549E5C111FA7A855" Guid="{7E341751-8BD3-4B67-8759-A3A4625B6520}">\r
+                        <File Id="fil68500F4B2DC90A64BA585E3D5E9141C2" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\FastGlow.qmlc" />\r
+                    </Component>\r
+                    <Component Id="cmpA9B83EF4B33682EFEA153198F1C44255" Guid="{26AF03C7-77EC-40A4-ACB0-7866EE13492B}">\r
+                        <File Id="fil7DF3A69E24DEEFB702D593D5E4B76C0F" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\FastInnerShadow.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp8960218D3E3E2D16BFEF14C48404B5D1" Guid="{71C7363F-2CB6-48D3-9FCF-41DE21F660DB}">\r
+                        <File Id="fil0BE696756A233B2576651FD0ABDF1F8A" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\FastInnerShadow.qmlc" />\r
+                    </Component>\r
+                    <Component Id="cmp4951B8E17EA0B3A6B265E2C0E893A50A" Guid="{48B858D5-8008-4CB0-B9AA-B3DB33C43E3D}">\r
+                        <File Id="filA527CD28BB0968D628976B78FEB0CC38" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\FastMaskedBlur.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp48FE69666F780EE04FC507C3AF73AB4C" Guid="{CCBEE1BB-277C-4B21-9D4D-8C512734CE6E}">\r
+                        <File Id="fil2EF59FFDCD107D97D22F5ADD8449CFEF" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\FastMaskedBlur.qmlc" />\r
+                    </Component>\r
+                    <Component Id="cmp2BD742974C0791D6BD1AE4F7E3646D99" Guid="{E43BE5AA-F4D0-4CA7-A32F-3D807202C8BC}">\r
+                        <File Id="fil56212E247BB6F393FBE83AB4A7EAE2FB" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\GaussianDirectionalBlur.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp36A30368477DCD705088EDB144FAA0D0" Guid="{73AF5C27-1693-4475-924C-6451ECBF0266}">\r
+                        <File Id="fil0621241FE97703659CD3D7D5C4ADE2D5" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\GaussianDirectionalBlur.qmlc" />\r
+                    </Component>\r
+                    <Component Id="cmpF8459C652CF5C20ED87F853C1ABE1752" Guid="{D85D4DD2-A83E-485D-A94D-66B892DBBC21}">\r
+                        <File Id="fil39F0CB1500963D3395433DA5DFC9B13D" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\GaussianGlow.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpA1791452BCB3728CD63E88FCBF971D62" Guid="{6835392D-29F4-494F-87A9-B37664364D81}">\r
+                        <File Id="fil3D91872D9408E151E87002A30A11AEAA" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\GaussianGlow.qmlc" />\r
+                    </Component>\r
+                    <Component Id="cmpCA0AF56CA87A52053FE61B6ECD156E35" Guid="{6E13F7D4-4D44-43F7-BCAA-A77EB901AF5F}">\r
+                        <File Id="filA28BA0B69A49D99C713AC84E1BE3CD39" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\GaussianInnerShadow.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp0F8FC23D5E3669050C7A79355797347B" Guid="{B5251E3C-0B3B-4155-BB91-A23FE75F7FB9}">\r
+                        <File Id="fil172F0C2476A5F0AF55072EC73E6E4430" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\GaussianInnerShadow.qmlc" />\r
+                    </Component>\r
+                    <Component Id="cmpBC9523950CF52702CB1A519123F9E519" Guid="{6F73A2D5-41FF-4C3A-80CB-05DE74669583}">\r
+                        <File Id="filE0A177DDE81AF18FB3CFE57A0727C02E" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\GaussianMaskedBlur.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpE2FE4AD9E8750F9DCD6AD8C03FFD973F" Guid="{0BB1158F-0EAB-44FF-88D4-F67F9B40DDF8}">\r
+                        <File Id="filBC86557C9AE14F08D5AD621C6B02F3C8" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\GaussianMaskedBlur.qmlc" />\r
+                    </Component>\r
+                    <Component Id="cmp302F033AE251BD62E648805EFB1BB536" Guid="{CDCAB63D-67AE-4A62-8FB1-9C0151617C46}">\r
+                        <File Id="fil51BFE00C51A89DC3E74DC04564BCB18A" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmpF3F704D8F2236A0A62B980FA41D3CB5E" Guid="{2CB437C9-31D4-478A-8BFA-9D8EBCD368DB}">\r
+                        <File Id="fil09960BC83EE77CD38744C380783E9547" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\qtgraphicaleffectsprivate.dll" />\r
+                    </Component>\r
+                </Directory>\r
+            </Directory>\r
+            <Directory Id="dir898F47F1C6F351486C9AAE4005F1B49F" Name="QtQml">\r
+                <Component Id="cmp00AC1A2BE52EA04246F38C5F0B22A9D0" Guid="{DC866072-ECA9-4CA2-9613-C25D77760BE7}">\r
+                    <File Id="filE33FDBE48E6FC8479D62000A1D4DB79C" KeyPath="yes" Source="SourceDir\QtQml\plugins.qmltypes" />\r
+                </Component>\r
+                <Component Id="cmp0A8C093EE03780F9F7823442F10A6C82" Guid="{109B01C3-36B5-4130-9A37-ACA89A7BB868}">\r
+                    <File Id="filFF531EAEA986913D79F95FFBCB9A3DD8" KeyPath="yes" Source="SourceDir\QtQml\qmldir" />\r
+                </Component>\r
+                <Component Id="cmp44B6838E73CD47A3F0061ECCE2EB48D0" Guid="{29B239A3-D455-40DA-8A21-F9AEAAEAA42B}">\r
+                    <File Id="filA5A3A998AB34A694D3B664958BD2376A" KeyPath="yes" Source="SourceDir\QtQml\qmlplugin.dll" />\r
+                </Component>\r
+                <Directory Id="dir48A5FBBF91E1C202003BAC3F8CB3BF51" Name="Models.2">\r
+                    <Component Id="cmp57A7D8DDBA7FB780E38A3CF0C3DE50F6" Guid="{FC1A6BF2-A27C-4AB4-BC53-31092E0481A9}">\r
+                        <File Id="fil6BB08B36A3E11C569382D84D1E88C9B4" KeyPath="yes" Source="SourceDir\QtQml\Models.2\modelsplugin.dll" />\r
+                    </Component>\r
+                    <Component Id="cmp74FB488898EE30854670C634A6CA57CB" Guid="{928C1715-2F0C-4BA0-9CEE-8DA1D740D679}">\r
+                        <File Id="filBEE366112102E123818634DA4D2DD766" KeyPath="yes" Source="SourceDir\QtQml\Models.2\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmpFF41000DFB8364F307DEC40444A8ED49" Guid="{0E33686C-EA52-43E3-83D4-AF793E79EA25}">\r
+                        <File Id="fil4EF76584838408469F9A719687A6816D" KeyPath="yes" Source="SourceDir\QtQml\Models.2\qmldir" />\r
+                    </Component>\r
+                </Directory>\r
+                <Directory Id="dirB873E81E5DE46F6AFD9FA4F4CB07FA16" Name="RemoteObjects">\r
+                    <Component Id="cmpF79607E800F2F8BD75203694AEAEF46A" Guid="{12A3CEE1-10CD-44B8-B7B9-3DB0C53161F4}">\r
+                        <File Id="fil0CCC725AAD4A4C3AF785A0E6C05B5C01" KeyPath="yes" Source="SourceDir\QtQml\RemoteObjects\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmp1E5895F7DD6F9A8239FBB2DD8A187651" Guid="{15329E23-708B-4A03-A52F-7A1D82FCE4A3}">\r
+                        <File Id="fil553CE70C26DE61B484FBA8843B1FEE43" KeyPath="yes" Source="SourceDir\QtQml\RemoteObjects\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmpBAB6384A9280EFB631A74D56E4849879" Guid="{17B1657A-A346-49C0-9FC5-261B190DCD78}">\r
+                        <File Id="fil9282D53CFA747D92DF590161D94F7C8B" KeyPath="yes" Source="SourceDir\QtQml\RemoteObjects\qtqmlremoteobjects.dll" />\r
+                    </Component>\r
+                </Directory>\r
+                <Directory Id="dir7F50D8639A90BC7F159C74A902C07A8A" Name="StateMachine">\r
+                    <Component Id="cmpE8348F57B7F7ED3E102061755EA59678" Guid="{2BFB9277-FB81-4D69-A8D4-94BF7B3492A6}">\r
+                        <File Id="filEA848B17E993426219C6780A6AD0EF90" KeyPath="yes" Source="SourceDir\QtQml\StateMachine\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmp789571C85C4C76BD48E9A471271B5128" Guid="{5ED22F6C-7FCE-4947-9538-4C89ECB8F4CF}">\r
+                        <File Id="fil4203D832E588C73EC48309F8D3AC2BFE" KeyPath="yes" Source="SourceDir\QtQml\StateMachine\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmp07E1D3988D9259288197F71CC0B0559A" Guid="{5D21407C-F3C3-428F-B397-9C4E0D666EF5}">\r
+                        <File Id="filFC54BEA29869B52A91A0A804A6843B4A" KeyPath="yes" Source="SourceDir\QtQml\StateMachine\qtqmlstatemachine.dll" />\r
+                    </Component>\r
+                </Directory>\r
+                <Directory Id="dir38701E48D40F53928079A0277B1BE418" Name="WorkerScript.2">\r
+                    <Component Id="cmp91EB04F6C5AFAF292A0BFCE3570DCBFE" Guid="{4D931278-DD8E-4ACA-B8E0-8436B303B917}">\r
+                        <File Id="fil3AD6975608C156FBDC0A714EFD2E7779" KeyPath="yes" Source="SourceDir\QtQml\WorkerScript.2\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmpDA0DD041411FA5AC356FAD21EBB0BFCC" Guid="{04B2D75A-13D6-47E2-BCE4-0E230B94AE5F}">\r
+                        <File Id="fil7757938E9EFF9E82F19FF17128644874" KeyPath="yes" Source="SourceDir\QtQml\WorkerScript.2\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmpC0C294F18C416B2F09D563A7D2F171C0" Guid="{0B5FC87C-D82F-46BE-AB18-E10BAC7566E4}">\r
+                        <File Id="fil585107DFD90EACA350C802AFD64551B4" KeyPath="yes" Source="SourceDir\QtQml\WorkerScript.2\workerscriptplugin.dll" />\r
+                    </Component>\r
+                </Directory>\r
+            </Directory>\r
+            <Directory Id="dirA71A8D5C38BE16878A4EB5F3B4E1599D" Name="QtQuick">\r
+                <Directory Id="dir1CF0A9FB12A9B3F0BCC43F64CAB69841" Name="Controls.2">\r
+                    <Component Id="cmpBBC07C7AA9E1D56538F0EE5BC883A106" Guid="{18653B7C-05E4-485E-9C62-31D4F2664E1F}">\r
+                        <File Id="filC6250BEA59F89937D08252CE2344D671" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\AbstractButton.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpA2AD6F575E441894DDF6301F935EF3E7" Guid="{6B3F6F40-74AE-43F1-8EDC-97CC40267B5F}">\r
+                        <File Id="fil7E4C91C7F6D6DE165F5E68148D5B85FA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Action.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp346061900FCE515D6B32CFCD7971325C" Guid="{AE4849A3-BD9E-4D75-84A4-F78DE4AE50E2}">\r
+                        <File Id="filB221D15EF9AD3699086FE43E3C355121" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ActionGroup.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpE5AC86A748B88CA71F59F050376D4BC8" Guid="{3D56EB30-C37B-4040-9341-53A74B36291D}">\r
+                        <File Id="fil8B70502E8A7420E0F20E1C107A446561" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ApplicationWindow.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp26A45023E109A672FAA78FE5C4F827F4" Guid="{F0BE556F-358F-4AA8-B311-8102DE486887}">\r
+                        <File Id="fil2FB69D0E22B7CC46B57FAB19D2931318" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\BusyIndicator.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp92014B7FBE8B02881A6A53384BDCD1C4" Guid="{65C1C88B-5B3E-46E2-965C-DCD4426EFCA5}">\r
+                        <File Id="fil29272699B19CB5F1A2B83C23066B00AC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Button.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp372134AB6EF24BAC7497CC47466F0727" Guid="{B8E80B2D-19A3-434C-A380-DA262254F876}">\r
+                        <File Id="fil9BAAB7742819691F86B3837FCA161A6E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ButtonGroup.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp17FB129D73D5BCA38BEC406E5A7A9853" Guid="{B4EEB089-7AA1-4EAE-BAF3-13837AA5AA71}">\r
+                        <File Id="fil40C33AA9302346640FF8F3431BDFCC16" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\CheckBox.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp59BA1197723375EE2B1A8FC0971407A8" Guid="{C256DDB3-D9F8-47D2-8702-9290C36AB4E4}">\r
+                        <File Id="fil1D4756FDB82E5F7C242AC3DC37F9B2F9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\CheckDelegate.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp9076C49961F4F0F4F5B4F65EF0CE7925" Guid="{6F747374-523D-4121-B79B-E0062B08B433}">\r
+                        <File Id="fil6A4AAD9D20B45D7D2DA5707F1390AF0C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ComboBox.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpCBF4FE8555DE1D61528AF79EA3C5870F" Guid="{26599BF7-BCE2-4202-8B5A-F009B999646C}">\r
+                        <File Id="filC5D80E96EA90020D98A1BA9C7A32D089" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Container.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp63B837CFDE5F5C8ADC92D6A80E367621" Guid="{F67247A2-2EA5-450D-87C4-AED6C9A8E069}">\r
+                        <File Id="fil82056DDF3A22A3C956EB50AD96DE681B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Control.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp73FC7C1B9421F67443A7CA5E03C6F59C" Guid="{2225B539-878A-40DC-986D-742CFC262E18}">\r
+                        <File Id="fil00CD17A8DC26B3D4E21AA1AA951F3910" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\DelayButton.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp24D33989EF8148F7E8E801168FA75D02" Guid="{88F2FD96-6F60-460D-BBEC-F0B0BE0CE94B}">\r
+                        <File Id="fil0F8C8B52CEAB25AD4FD610A146431D1D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Dial.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp781F94BE6B67AAB9F2DD4A6865F67043" Guid="{12F45E61-A438-49AD-890E-BF5AF913BAED}">\r
+                        <File Id="fil9429984A3AD047F645C68743E4853D3E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Dialog.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpBF8FE1999E64BE53CD0FFE028F2BA8E5" Guid="{C825307D-6856-48E9-952F-D8DD4371E086}">\r
+                        <File Id="filB66714CD3CC8044DE03BFB0C2998771E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\DialogButtonBox.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp508D7E9DE20A057C80595708342CCD11" Guid="{79285B58-ED37-4B36-81A1-2F01EE9A8D30}">\r
+                        <File Id="fil4951F0E0C838BCE74B6A4FB412B33C39" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Drawer.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp814DB5A7174FAD3D492689B3CFFD4FC6" Guid="{C5568152-77D0-43EF-BB9D-0066F205825E}">\r
+                        <File Id="fil26FF980D9A1E103DAEF78DA89A8B39AE" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Frame.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp9FCA0D048C5FEDEF35CFDCF4220D148D" Guid="{552D678A-1984-4C88-89CF-42371097B8C7}">\r
+                        <File Id="fil7EC20FB2AF608FD4D48A9F19F1B92A7D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\GroupBox.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp8302E43F7B461A53125D24EBAB2C8BBA" Guid="{9908593A-2DF6-4B1B-844E-22E9D4AF8FC4}">\r
+                        <File Id="filA490553F561E23B426ED5F47B779E827" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\HorizontalHeaderView.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpDD4764A31C5966C382C9FADCF2747DAE" Guid="{A8C30115-60B3-4519-BCC1-668717555DB0}">\r
+                        <File Id="fil20BF35D7A8C7028231C16A4F8A8B8E7A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ItemDelegate.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp4DC6229A880E85A8EE64B69A175EB7F8" Guid="{59036E4B-ACD7-4733-B14A-EBAA93E714E8}">\r
+                        <File Id="filADF106B97E56C6627AF27930AA08B829" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Label.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpC3E4C72480B21745D1D55462CD128148" Guid="{B1B90609-C99E-4B1A-8B10-13BE83B179BB}">\r
+                        <File Id="filCDF998E5B0CD0C7D86DEDA98E71F235F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Menu.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp9C2C5E7D9EAC8AD7E9FB8BA9B0147BAB" Guid="{BB4200E2-ADED-44D8-A1A1-31A0E8707754}">\r
+                        <File Id="filA5709A532A6415760B623CBD689B3F88" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\MenuBar.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp66ADBF15C183C1EF05BA59EBF57DC04C" Guid="{592C458A-E1E3-4735-A22F-92B5FEECBA7F}">\r
+                        <File Id="fil9818915B5CDD8FDC7B9B723C438DDA64" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\MenuBarItem.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpB85DB993D3D58E717F4EC2A2A5AC4130" Guid="{604A7793-4EC9-4F5F-9BAF-DE22555BFF3A}">\r
+                        <File Id="filFAE639B09FB48AA6FC953557339B53EC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\MenuItem.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp93014FB27329F9718833E30A8DCE2F23" Guid="{EF1D6D21-BDC6-4DDF-9BC0-875EC3FAE40B}">\r
+                        <File Id="fil09A4B643E4AEAFC3F22479FBC0E09388" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\MenuSeparator.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp9D0CC60826A95D19D52A3A08FB44155C" Guid="{DFE9585E-6220-430F-9F1D-1B2130D88FF1}">\r
+                        <File Id="filDFFEC8756E4A921EAD31C7CD841BAC7B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Page.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp06BD88C78B72DD375F0FD8DC37812A7A" Guid="{9DE25AD5-FA94-4DFB-B129-0FFBF312E353}">\r
+                        <File Id="filDC61FDA5B2042E021FC28DFA1C464ED4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\PageIndicator.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp6577B61F7B803092F263CE9BD76D7B19" Guid="{D6D9670E-CD58-40D4-B772-C8AC087E74B3}">\r
+                        <File Id="filD74C331BEAD78D4B70AE295DE481F56E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Pane.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpD880E6EDC6A23CDFF94AB5E0A0018A10" Guid="{A3989AE9-2FD6-470A-B8CF-78D908DBA2E6}">\r
+                        <File Id="filA2017FDECC21B1BD1F3A42BBA6781F78" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmp00D081BB0762FEC39933D1C2960FC831" Guid="{ED79D960-6E3D-4B98-91D1-376FFD6D0C55}">\r
+                        <File Id="fil3BF8126A47717D54F2FC8B0E9EE88747" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Popup.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpD69A2401CBD6D26E44BE1271C8905A24" Guid="{0231D127-E285-4F07-8AB2-AB26F8850A75}">\r
+                        <File Id="fil7827CED2892E4BD2A7216EE8DC487134" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ProgressBar.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp0B7DD63BE81BC1EA8DB09A99C12CB156" Guid="{A7BE0958-70D3-4C14-82E3-FD911E5B2EAF}">\r
+                        <File Id="filBF429532FD2FB957D749DBFBEC0C1781" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmp1EC09C5C4D6CE477EAA2BE5A3798E1CC" Guid="{BA7E711D-1F6E-43CA-9957-C06AF625F0F8}">\r
+                        <File Id="fil8A3E57CDCB38EA2957526857F4BF7833" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\qtquickcontrols2plugin.dll" />\r
+                    </Component>\r
+                    <Component Id="cmp577EBA2061AD5D464AC5B629EF1A63DE" Guid="{248F67F3-A6F9-4619-B355-BB41E97401D1}">\r
+                        <File Id="fil2CF3A6D6E008AC1C946D645F7484E0AA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\RadioButton.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpFD9033FF82B141429428B759027C0ECB" Guid="{E0382F5D-D17D-4955-81ED-046C720C45AE}">\r
+                        <File Id="filBBA1B0E513D4C6E93929431510FFC51E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\RadioDelegate.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpD9A7A9076E4844ACEB4385E651C6FC27" Guid="{8B8F1FD8-F041-431E-BC04-94955681EC8B}">\r
+                        <File Id="fil92EBFA0D398D93C208D55E0F2DE5C412" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\RangeSlider.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp952D38FA5543D46E8B0B6CAC867E6D19" Guid="{6E73EE8C-FFF9-4D5E-80E4-49DCE2750DD8}">\r
+                        <File Id="fil3BC533F32E89455AE3C47B34F2D990BC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\RoundButton.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp8360F4A2F8333FCC0A95F36461DAC0AE" Guid="{6FEA34D3-39E4-4BC0-85DB-ECDF94B77477}">\r
+                        <File Id="filACE9F613C573BFC25A9A0E48B6C143C3" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ScrollBar.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpA8C48D6004583C73A28027E805ECF241" Guid="{FB1C3A2B-9330-4EA7-A34B-25612A2C2E6A}">\r
+                        <File Id="fil7BD43F79B053411805FC22E0BD5D488F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ScrollIndicator.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpF0F52E0AA644EE5B8A4212507A2A9632" Guid="{40D7143A-FFDF-44AC-ACD4-00A6A5464BE7}">\r
+                        <File Id="fil89005ED22EF40F5C060D464B8096616E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ScrollView.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp1F2A598B5F572A95F619F914ADFDFD01" Guid="{3536777D-FF8B-4EEA-B93C-5EE1301E5039}">\r
+                        <File Id="fil1C7BF5B291EA31328091F13B845FED6A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Slider.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp894CC6997FDC49D32993AF049D19661B" Guid="{30F4723C-B159-4C29-9EA0-C8470CDC3204}">\r
+                        <File Id="fil298110AF9B2ED52166A7A0ACE2C9EEE9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\SpinBox.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp45A86D8F1E2A0DB9085F1C386BE8C441" Guid="{361C717A-E7E1-48E2-B5FD-E934ADF6C5F5}">\r
+                        <File Id="fil7E7C8E62F5F6317023CD5DDFBE502AA0" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\SplitView.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpD7F91C9BF1B37E7ECEFC803745680533" Guid="{2972CE14-8B1C-44AA-B8DE-80551FE96662}">\r
+                        <File Id="fil6479652BE44E448F123964A1BFFDC025" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\StackView.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp6C7380F6F9D3432EF0FF68155BE5B7EA" Guid="{D8A7D60B-63A0-4AD1-950D-055177620665}">\r
+                        <File Id="fil2616AA919B65C7E1223B8EEAE8A0A520" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\SwipeDelegate.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp163F0198648660C0C4DED70616CD3A50" Guid="{1FBF4796-E2E8-4ADB-A545-4BF2807D20B1}">\r
+                        <File Id="filCF410250FBBD03B7BDF88E86CDE7F5FA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\SwipeView.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpF5CA7A4D3DB71A67C3E4BC87D9A553DE" Guid="{81DD052B-A012-47F6-B62F-EA0E79AC7846}">\r
+                        <File Id="fil263B8E09CAABE937FBEBF1178E2C8264" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Switch.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp2599D94F81ED612C05479E9A8F58A189" Guid="{7DB969D4-59FE-4E04-8D11-E3BDEFB46738}">\r
+                        <File Id="filA4030299DEE7EEF863C5BDC4ADA0363B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\SwitchDelegate.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp9557C3464EC921CAC1A43D77B20BA92D" Guid="{DB31EB03-0A74-4002-B530-18B87D842D6D}">\r
+                        <File Id="fil13E41D7725EECC41EB76F1C55FB37034" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\TabBar.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpFDD06D370422F04997F37CDCF9077AAF" Guid="{8EAE7AAA-839E-4BD7-A7D2-135517DD05B0}">\r
+                        <File Id="filC1B3E77F09168E169AC46F312631A9A5" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\TabButton.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpC5C6ADF92BCA401BFD4AB6B8DF07ABE6" Guid="{E2A3B225-AEDA-4573-B8CD-F3F84AE9A8A0}">\r
+                        <File Id="fil8804492F60CD4E2F68C71219E02292EF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\TextArea.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpC7F75CEAF1EB837B6998ED5DC6550404" Guid="{01405B24-2CEB-418E-9B59-5F96A26C9D50}">\r
+                        <File Id="fil6F19FED3B4412AE010061DF7D4BD3DA0" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\TextField.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp4E0B2D6970453AF152BA060236BD8B71" Guid="{5ED9BA2B-AD36-4E01-9655-9EE6A765360C}">\r
+                        <File Id="fil340E1F0612644D4DC4EADFE9E87F24EF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ToolBar.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp3A32B8F13C49569A0A4AD1D11045BE3A" Guid="{A7D20B7C-B367-4014-9FDE-1DFBDCEB6C9B}">\r
+                        <File Id="filFBEB8E9016BF58F623173CDBF130428D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ToolButton.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp6D11E1D44ADD3049CE735F324F7F4521" Guid="{A972070C-BE88-401A-8BCC-8C6F4C4D9071}">\r
+                        <File Id="fil18978F440E9B6E6F62CE45EEC31C0F8F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ToolSeparator.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp6763D9C183B24CD6659B616DD72994DA" Guid="{BD007679-0CF6-49ED-B858-D57788EFEA1F}">\r
+                        <File Id="filB20E33BEBD71D625CEA0199980F77F55" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ToolTip.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp0FEA3716333AE56EBB72D727432DFB0A" Guid="{1BEC8363-459E-4097-BCF8-724A52CCFD53}">\r
+                        <File Id="filC8F8B2D82FDC1ED4350CA76D702241C7" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Tumbler.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpF77FF7EB150083CBBB82946EC3F849C5" Guid="{77DC42B7-660D-4EB2-9553-1A8F3A054CB6}">\r
+                        <File Id="filD758C10071B91117472250A6D6E5C07A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\VerticalHeaderView.qml" />\r
+                    </Component>\r
+                    <Directory Id="dir035355762BDDA6A88B77A84B585AD0EF" Name="Fusion">\r
+                        <Component Id="cmp8693A45A9517FDC707611CD9592AD887" Guid="{FE1A6B72-4416-4058-B8DE-FD0ADA5E00AB}">\r
+                            <File Id="fil7A76573ABBE1FDF6842F852511E36ED4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ApplicationWindow.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp22009CC98E7BCD73739238DBAD70F5FF" Guid="{EE86CB3D-757B-4591-8A4A-71DFB47EEC98}">\r
+                            <File Id="fil4D6655B7651D7B655CDC465A96569C20" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\BusyIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp006248D9C946635CD2C0D21D711BAF97" Guid="{71322A32-8BC1-4BC3-A2D3-43877F86E8E4}">\r
+                            <File Id="fil4677B542408030BF8A7E55B148B934EA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Button.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8F4980EC013CDADC9677179C1B5FB4DB" Guid="{9C60553B-BC7A-4E02-A56A-58833FC75143}">\r
+                            <File Id="filF44FA16699EA690B580F758E29738AAA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ButtonPanel.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp7CB5E11B511D00866734F2A59242071E" Guid="{AE591101-9987-4988-BB53-276AC7E46073}">\r
+                            <File Id="fil3CF03534ACD9304F06C0597102C48388" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\CheckBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp15339C0E193CBE17FB6CB89DA971471D" Guid="{E5A1CF93-FF79-4BFF-9429-876C326CD587}">\r
+                            <File Id="filB933AB67B806FBFAEE5B4C6EEA24FD2A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\CheckDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp7A634DCF2DA1EDFF9FBAF699758570C3" Guid="{E8EFCE73-1AB6-466D-AD08-C8873EF95506}">\r
+                            <File Id="fil3FDFED2EAAAED2687815DA9661B0C595" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\CheckIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpAA64F717D23C1375E79BE28E5A3DE005" Guid="{EF2F8A2E-4BBF-401E-A1B7-041B7329F812}">\r
+                            <File Id="fil95F73162924608F461FCFA52B66A0A6E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ComboBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp14F754092A68F3FE755963981761F031" Guid="{E0E26331-F0B7-4DFF-89E5-8894EF16DAC2}">\r
+                            <File Id="fil96C211B2F831A30A5AD62EDC6CB0AE86" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\DelayButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8DBD1F81C17E5D01D819E619D30E83CA" Guid="{242B321E-98A7-4685-84E9-8C8BD6CAFC92}">\r
+                            <File Id="fil1CC8736C80CF69BDCE8C8B989F215658" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Dial.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp9A97912EFCCFD956EF8F155A81CB494F" Guid="{D2C607E0-8E4B-4F55-B60B-024D8BC34882}">\r
+                            <File Id="fil9912627C86943446F991D141E64E84A3" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Dialog.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp37BA96169B5087F3D2C6E769FF5E9270" Guid="{B5153F7B-2A7E-4FB2-B7EA-C68D043B4F76}">\r
+                            <File Id="fil9D217918F4DE6A7DE53939E61C810E92" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\DialogButtonBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpE74780CCB3558FB4C37DDE65264DC1F4" Guid="{4F45062D-232A-4157-ABD7-9351742C3994}">\r
+                            <File Id="fil404168979C602A6C3E2A8E77521843CF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Drawer.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp86D5D0E9BB2DE2BDCC6CF459CB7C4E01" Guid="{6FA92936-097C-41B0-A5C2-E7DC100A7908}">\r
+                            <File Id="filCFFF61300EF689F96CFDD9B3A8F7B83D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Frame.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp3951161EB256315C232D7C2B211A5D42" Guid="{FD64BFEF-8970-459F-843E-9D4E07A8DEBD}">\r
+                            <File Id="filAC8A24BE65028BFAF11984F7DB0B168C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\GroupBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6CAF0B6CD8ABE85E62A160036392A2CC" Guid="{88F69974-2FD0-4D7C-8E3D-36CD596B9459}">\r
+                            <File Id="filB0D55ECBE8B840F45E54296721149C79" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\HorizontalHeaderView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp55E217C43921E96FF82040712469E0F3" Guid="{D471DAE1-7C90-4202-91FB-0BE5C9E77506}">\r
+                            <File Id="fil2D7C328BD2280D7E0E3ABE5468F4CEDD" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ItemDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp57277D9EB23963010750499ED2F541B8" Guid="{1096E01B-7229-42C2-A357-E3C45DA54168}">\r
+                            <File Id="fil3FAB3A32AA7A668FFB5482A34769E564" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Label.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp651F7330940689374F3F4500C1F20484" Guid="{0D729967-F2B6-44F7-85D7-461595625C6E}">\r
+                            <File Id="fil2E6E1F91E7A60FB5D16E4C853B247E7A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Menu.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp02C5C41CDED7F94E885A4A2A28EC3600" Guid="{F4E65E26-2552-46BA-AF61-C547429D8F01}">\r
+                            <File Id="fil46C51416AFB762FFEE81E338F1212F54" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\MenuBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp95C32716B4F28A90B070E08CAC81BAC4" Guid="{7B371B0C-5AD7-4E93-B5E1-173CC26FD81C}">\r
+                            <File Id="filDA216EA9FC2E71295498C5082FABF4AF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\MenuBarItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp2CEB59611FF3F33F7AEF7D2EAAE0CB05" Guid="{2F5B5AEE-CA5D-4579-AE12-C90238707035}">\r
+                            <File Id="filCA0597907941481E694D27A9151A67F3" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\MenuItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8D1031BFABA660F80B7793BB3761EB75" Guid="{E7361D28-855B-4961-8AA5-71A3E6BD7F08}">\r
+                            <File Id="filD9334919AAFD0FAA024307105F6E2274" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\MenuSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB52CD2CEE9160F5F67372170543F85B4" Guid="{3D125A08-8FBF-4F5F-8632-A08FD9F18975}">\r
+                            <File Id="fil549E037AEFA7D53E8ABEFA528585B365" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Page.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp909217E4E53799CCE36ED3F7B817DF15" Guid="{7CDFBC69-1743-4B4D-9D6D-CC727453AB80}">\r
+                            <File Id="fil622B076063785FD360184B7CCE9C1963" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\PageIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp88C4BDBD3D48F447FF34FD6DF1FC3C41" Guid="{8249DEF8-38C4-4F19-974E-6E455FB45D9F}">\r
+                            <File Id="fil258E3E262CBA06FD489EBCE314036F9D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Pane.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6D134750A96C36CCE37CDAEAC146A810" Guid="{AC624DFB-C752-43D2-B643-A18625D081A8}">\r
+                            <File Id="fil1872A6AD9CAD77362D7D04C024F76B25" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\plugins.qmltypes" />\r
+                        </Component>\r
+                        <Component Id="cmp17431F800533AAD492A8DA91C537AEE5" Guid="{CC784F75-F543-4123-9E10-3097581F6EE6}">\r
+                            <File Id="fil5955CD9FB6B733D9B586B52CCD59F161" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Popup.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp5651D28BAA4D7517AB64D608440B8947" Guid="{0E1DA31F-123B-411E-A310-910106CF7103}">\r
+                            <File Id="fil1F6E4572AA5C15F6F2F86B70AD845F39" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ProgressBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp1FC8B77EF51F392FB0F6CD269098431D" Guid="{921782D1-C60C-4012-B36C-D44FBFB1A9A1}">\r
+                            <File Id="filDEC4823160DF80F031FFEDD16C5FA73B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\qmldir" />\r
+                        </Component>\r
+                        <Component Id="cmp403E7038C7324A61D8727DC1D939E547" Guid="{F851CBDB-DEF2-4DA0-AB32-F038CA4D94F8}">\r
+                            <File Id="filD279E9A3D628C42CFDEC99CA9E5A2F3D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\qtquickcontrols2fusionstyleplugin.dll" />\r
+                        </Component>\r
+                        <Component Id="cmp98D46B3DC15B713ACF5F53B34EE13068" Guid="{8C570892-F30F-4CB2-AC3A-47382EC6B368}">\r
+                            <File Id="fil50FF1AE0E3D989A4CA249AAF9F8A7B26" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\RadioButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp82998B9C0403FF9F31583386A05AA9BB" Guid="{74DB260E-F5DF-4426-BDFE-14F1B954C661}">\r
+                            <File Id="fil1A72CA8DAF4CE929C79B9574D2F30382" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\RadioDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0D0B1F4459593A8B3D4A1BCBA139D98F" Guid="{FA5F9B73-E11E-4A44-8F7F-1C51E7F48E5D}">\r
+                            <File Id="fil46018A40ED31116940522AF13C6F1FBA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\RadioIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF083A0EDD7D82B7EEFA116A49700462C" Guid="{809F9508-ED4F-401C-89F7-D01F181560F7}">\r
+                            <File Id="fil3E28D7DC0567E0CD67D51AB0878E1837" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\RangeSlider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp094723DF4DF9079CABB07B397F53FC14" Guid="{75821D85-CF6B-464F-90B3-72300BC5245F}">\r
+                            <File Id="fil0C857F8E3C8E3F5D9E839C9D1A2D1BE6" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\RoundButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpE46F1304BE1F25953FA1502338B46B6E" Guid="{E57FB784-7FCA-4108-8A90-43C9DAC92E74}">\r
+                            <File Id="fil0A615F671CBAFA25B83B6DDC1A57BE65" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ScrollBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF34990BAAB80CEB1B1FBA3F75A8BB18E" Guid="{BA75016B-BCEE-4639-AC14-960BBBA684CC}">\r
+                            <File Id="fil4926BC97FC5BF303193B003042C8161D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ScrollIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF8164C6D715D603507F60EA7662104BB" Guid="{3DF8922C-C3E9-49D8-A369-F8D21BFB1D94}">\r
+                            <File Id="filBAF585899E98CD52881FF18EA3FB54F6" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Slider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpBC49C3C8C0D1C882B8E4BFA5DDB84097" Guid="{7A79B435-9227-413C-9444-137D88C7B937}">\r
+                            <File Id="filF5CA1B27FCBF261A45FC19EA4D7BA023" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SliderGroove.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp026A749908FF7AE58055B0E1AB393168" Guid="{DE24E255-176E-4087-AFAC-E6F56B5D5EAC}">\r
+                            <File Id="fil929EAB5C19264E6900242F51F02822E8" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SliderHandle.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp4644F2A5A8ADFEA7ED73D09A41CB9AFF" Guid="{2A5E1174-6741-4BCF-BBA3-CE6FE52F79AC}">\r
+                            <File Id="filBA9EEEC5C7078CFE9E1B0DF8A177B5ED" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SpinBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF0E7DBBAED60C3A0D5E4980C6384C524" Guid="{5DC166C0-8A39-44D0-A9B8-FF474AC6EBDA}">\r
+                            <File Id="fil15BD010BEC93BDA46E1F2686D8FF6EBC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SplitView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF26383485D9640683A7276A7CFD06280" Guid="{BF3FBD86-AD76-426B-A685-CDF8FE76CC5E}">\r
+                            <File Id="fil55D360EE1ABEF5F5BD900B65B188D41F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SwipeDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp5D85AEBC11476CF382025D6654F36103" Guid="{029CDF92-C9FE-44CF-B9B6-E4A1673AB30A}">\r
+                            <File Id="fil3EDB2B2C23697DAF9F92FFBA99A84650" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Switch.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp44002125F963B5A1D193E0D787FF6AF6" Guid="{04725E3F-4601-427F-9A74-36F09BC8A604}">\r
+                            <File Id="fil50318A9E962C21697CA18156BA2D0905" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SwitchDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF8E1020B8AEA4B8F647DF3962BB9CF2C" Guid="{FA2434F7-F3ED-4B26-A434-58E112C46E31}">\r
+                            <File Id="fil921E1EA2463721903BCAD40702BF03D1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SwitchIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6F5B3D19FBA609A3ECE0700E2DB1C805" Guid="{9F1F0447-B481-4AB2-96BF-259633CF6889}">\r
+                            <File Id="fil8B653517ED9597AB20ECDD5E3A75C756" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\TabBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF0883486B5B318E646462C05EB5DBF23" Guid="{E121C66B-7244-4EAB-8225-A363D938EB07}">\r
+                            <File Id="fil17982E06A90DCC3F94109971D657D754" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\TabButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8F7277DF6FE75EF54431CCAB073199B5" Guid="{D75EED9E-1240-47D8-9DC5-5F5832E061F9}">\r
+                            <File Id="filE3100069BCDAE1F6526502196EFBED22" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\TextArea.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpBC3A73EE4B192F1BF88AF2E61E4212EA" Guid="{B137AF9C-F8CC-446F-B263-2DD5A818E699}">\r
+                            <File Id="fil3563463C4219FC7B6DFE6A95F3E9B75A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\TextField.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD48A473B45218C0BDE5C64912EFCBAFC" Guid="{2106F73A-F03F-4BEA-A2B6-D8B6C8632E15}">\r
+                            <File Id="filBA1284C10E3F10A1ECC1A687F9997CB4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ToolBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp86201E518FC2273E24BCD76E296C18E1" Guid="{1027D379-4165-4AC8-B902-6ADAF098ABEB}">\r
+                            <File Id="fil8B324D3244F760FD93C00D0E58BAB57B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ToolButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpFF8739D6C4BEC98E8F1240FFBD0A64B0" Guid="{AAD9D3FB-1258-4397-8DB0-703866C2D3CF}">\r
+                            <File Id="fil47FA961107A0E38C1F9CC57AAFD5CC5C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ToolSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp38720475F2109CBC7EF4F43C365CC5CA" Guid="{BCF56568-1D7B-40A5-ADCE-DD4376A42888}">\r
+                            <File Id="filB001EB30F35E5F650A63D03CC18ECE35" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ToolTip.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp996D8C56417E10EF44329F7339E474F2" Guid="{1C26B967-00BF-4D70-8844-0A80BB8FD464}">\r
+                            <File Id="fil93134119EC59B8D442C2BE7A07DD5D2B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Tumbler.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpED22A8F3ACEE04AB6CE44D533F4F300A" Guid="{72F16F90-779D-465A-892E-3B55A338F3AE}">\r
+                            <File Id="filE8C20555ACA351E1325A91CF709D8D00" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\VerticalHeaderView.qml" />\r
+                        </Component>\r
+                    </Directory>\r
+                    <Directory Id="dirFDC25EE37CF6C040EF6B98E257279EC3" Name="Imagine">\r
+                        <Component Id="cmp838B8853704ACB2A7F45BA00B950F3BB" Guid="{141E5A2D-23F0-4522-864B-D3456E2280FD}">\r
+                            <File Id="filC7285D9D5529D85E5E0E217C2D16EA81" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ApplicationWindow.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDF9CC0BB490A823FFADEFFBDAB92C2C7" Guid="{5360026C-8AA3-4FE8-AF6A-CABD86B2B70F}">\r
+                            <File Id="fil9835BFE6BBC02870FA9F628A2E0DB6E4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\BusyIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp73DF7086A467801478C7321A8B435AA2" Guid="{37150093-DBE6-4270-81A5-8D100EB60F79}">\r
+                            <File Id="fil5DA765828D51355DC019424A172FB9C1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Button.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp10D7AC86A56DE2D04356715F35250435" Guid="{4371D289-F3B2-4B73-9E56-9903FA7DCFE5}">\r
+                            <File Id="fil9CB23D5FD7586E7DA94F805DF1529360" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\CheckBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB8BBD8CCBCDE8771AD9E3D6D456FDD6B" Guid="{744F1464-6E9C-4793-981C-73CB5510C194}">\r
+                            <File Id="filC24A1ADF38694A93849B2804D4415C22" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\CheckDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp5DA5D6BC1A869E7409CC08ACDEBAA08F" Guid="{6769AE38-D978-44BD-A8C5-113A1244E476}">\r
+                            <File Id="fil3F2D3CBED068F4115DE0002AAC205A4F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ComboBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpEFE26691A6CBD74691FA18F89319520B" Guid="{50DC9AD6-A6CB-47B6-85DC-1C319A7F6F27}">\r
+                            <File Id="fil1164CEFD82A18A964BA86D08201F9CE8" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\DelayButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF14CA7FF23A8FEBB250D9C1A4DBAB5F2" Guid="{6102C842-3167-40B0-A897-7A562F821FA3}">\r
+                            <File Id="fil535F2A996B2C4FC84039F35960BC8B8C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Dial.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp76B901918023AFA1ECF5AFE1C5AADFD6" Guid="{B925E4BD-BDF8-419D-A21A-C4F593D261B6}">\r
+                            <File Id="fil2A26AB8828D225E79420414A7C55B95B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Dialog.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp3B04385170209415EA0392D6C6D9394D" Guid="{FFC78D64-1508-495B-A0E2-0A2290A40E05}">\r
+                            <File Id="filBB42C691204A19DFDDA38926D9A52E56" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\DialogButtonBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpAC7FCF48DC2DD2FF598D8D8EDFBDDE10" Guid="{68D0C46D-1AB5-4CA5-85E0-2175ADEC7360}">\r
+                            <File Id="fil152D23A298E533A4987600002CF20A9E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Drawer.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp31295E08BF0ADC9F7414B879A59EAB38" Guid="{EA9C98FB-2D3F-4970-95A7-2CFE7B8C3FCE}">\r
+                            <File Id="fil652FE91BEC083A5CA0F16DAC5115ADD1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Frame.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp26ED7398D2B7FD7CE79B20C6F81AA21B" Guid="{C1F1452B-9C22-49DA-AEEC-466D9615DB99}">\r
+                            <File Id="fil4E530D55EE5754394244F29DDD5A4339" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\GroupBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0AF138C48EF9389FB53BCFFF127C63B7" Guid="{99C63B6F-E80E-489E-B320-33CB9324A31C}">\r
+                            <File Id="fil7623E549EBD32B153F482A9ED3D10247" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\HorizontalHeaderView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB84DF029169B6913288463C0CD791FD7" Guid="{0982DCB5-B9A0-4CEB-9E86-DB3A55620327}">\r
+                            <File Id="fil47584D2EBEBF080D3E283A08BD78771C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ItemDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB2768FB92A037D37B0EDB0A50917DB6E" Guid="{C49D785A-76CF-4A89-8FF5-67F8DDA0A0D8}">\r
+                            <File Id="fil85E8BEE2FF4E7D88271DEE95F4B021F6" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Label.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp60DE9C3DB4C8AE187B4D19BDEDC6C16A" Guid="{A7D686DC-8D8F-4482-96F2-6D0C4F5BCFF0}">\r
+                            <File Id="fil21654FC5F7960A777690B781D204AE5F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Menu.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8439239F2F45A8EAE3D84F080943DDD1" Guid="{048C9830-5BC6-4C43-8E25-3C6B349700F9}">\r
+                            <File Id="filB0FB084CE7E619D3AD1FCFC01826933F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\MenuItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp16223EE59A9E78D62203D037E29D698D" Guid="{7B56130B-F94B-4A3A-B065-E6E4B66A9C20}">\r
+                            <File Id="filF1296202126A8FC1A5FF68833D23C1DD" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\MenuSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp018AFFA2C882E3A63FE78C48058C9701" Guid="{76589C5E-289E-46A5-A080-04A8D1FEC084}">\r
+                            <File Id="fil16A10C6B3AC90B78F38FF79B36A1D633" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Page.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD9344D05ADD481EACDED9FFB5EE9D160" Guid="{CF6DB635-DAE3-4E6C-8A89-3F5145779366}">\r
+                            <File Id="fil1D2EF351CF8F0C59F0CF7CD0CF748B0E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\PageIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpA9175F4256E542F0F7D7729B65AC8716" Guid="{1B82A4FA-CC2D-45D0-8871-6FDF98408E5C}">\r
+                            <File Id="fil6ED79940CE9EDFACD1AA339B928CD411" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Pane.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp022C885B9AF26895C6BBDBE1AF964DA7" Guid="{D7776A87-CE99-4B5B-BDD0-A6ACC91624F5}">\r
+                            <File Id="filD12EF181236C4FD2D8823E2A06702BBC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\plugins.qmltypes" />\r
+                        </Component>\r
+                        <Component Id="cmpB42CC102A79539E49D9D71AF819BDCFA" Guid="{FB36BE43-4D52-4433-A6B5-F2915F1CBA12}">\r
+                            <File Id="fil9D3116A765C1FFBA2E5F4AF29524555B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Popup.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDE27E4C6B7C8141F7B15529523211BDE" Guid="{5EBA816D-DF14-4E34-A151-FA79A354623A}">\r
+                            <File Id="filC4C7A697145B9CE44EC9CBEE5AD5EB56" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ProgressBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8A1FE21942F63A12FE8C262291D2F118" Guid="{10CFD156-62EE-4E2A-AC14-CDE672E214D2}">\r
+                            <File Id="fil8D5351D7FEFC8E0D7B074BCD4BE7E93A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\qmldir" />\r
+                        </Component>\r
+                        <Component Id="cmp89930EDDF65A84FB647750A3889D3998" Guid="{242DF3DF-4813-4AD4-800B-4A6A7F1E00BA}">\r
+                            <File Id="filE4098FA13958E23AF469CE23DE03ADB7" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\qtquickcontrols2imaginestyleplugin.dll" />\r
+                        </Component>\r
+                        <Component Id="cmp7A7C588B21E8E3817983B40E6C15A8DC" Guid="{9E76F7E0-607B-468F-8E59-B2614AD30602}">\r
+                            <File Id="fil09BC6622E343777DE250D2F47E297ED1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\RadioButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpFAF5864C58442FAA1D5C0A01C83FACE9" Guid="{202BAC2C-95C9-467D-BEB4-1EA7E27E31B6}">\r
+                            <File Id="filB5EFC9A07D2D7FD06A43503D3A79870E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\RadioDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp48641EDC547835A4C015C138C9EE12D6" Guid="{EC581895-46A3-4CFE-AC75-5FEB7C9C2ACF}">\r
+                            <File Id="fil977DBB2F4A4B7DFED8BB0612A7037970" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\RangeSlider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp74D44A9BA04153CCD0A6FA413CA57C4B" Guid="{7166BA46-DB87-4DE6-9C1A-1EB28816178D}">\r
+                            <File Id="fil11ADFCA60637AB6A2C391D9EA6B3C543" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\RoundButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp84CD7E65AAF5D3AD9F421445901CF37D" Guid="{CEAB5076-3A76-4680-AC7F-379B2329E9D8}">\r
+                            <File Id="filD9610FFD3B120D5B16F73DC0CBCFFD2E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ScrollBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpAB0EEB7FDAB89DB33CC1872F2D020B9A" Guid="{21EE7B59-9A55-4660-8849-2BAC9C1651D3}">\r
+                            <File Id="filAE6B882E064E88D269983EB2BF4F7EFF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ScrollIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpEA5C0DF0E7CC7FF0C0A116E8ED0DFD39" Guid="{4B3DB6DB-BDC4-400A-8559-032E988E2B47}">\r
+                            <File Id="fil0EB5F1D6A344D46C999C749965A81DF7" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Slider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp48F6BA7469D6C5E8A90EFDC2F1F12A03" Guid="{2F7713CC-29A6-4EC2-AA4B-E2E2970448EE}">\r
+                            <File Id="filE4BBEB9676EAAFD512614F5B243CD4F5" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\SpinBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpEACB1D29768FC4AD7754B7A8BB5CEAA4" Guid="{D905B9C9-ADBE-4760-9853-EFD1EC7FBA99}">\r
+                            <File Id="fil2F0169274B3846093E9CE0E0CDA3914D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\SplitView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp40DFBE5C4004C66BD421833A0916A402" Guid="{03C419A3-AF46-4CA9-A7C3-8ECFCD57104A}">\r
+                            <File Id="fil8E7130A25B7C8446BC71EC968B0715CF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\StackView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD46A2953ED55E92D9E0462BE4D66B828" Guid="{FFD1F1D1-32CD-4E77-ACA3-580CD5596381}">\r
+                            <File Id="filCECBBB01A4F76F4742FDB628AB2398D7" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\SwipeDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp9BBFEBDFBD798ED64CF1BFB852E57719" Guid="{45F72B4D-8EB6-4BE2-B31A-C9BF307DD2C8}">\r
+                            <File Id="fil3A23A94C3ED39F75C988138BFBEEA2AA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\SwipeView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD60B739F43AAC84324A66CEFFCDDBDFF" Guid="{E0F387DC-7D7F-432C-8EA9-9ED7F865D6CE}">\r
+                            <File Id="fil4751E171C8A0EFA145946D3D11DED958" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Switch.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp4AF8FAB5F76A012B1E00956784FB86C0" Guid="{DC4C5366-E401-48CB-9A6E-BE7F33FA39E8}">\r
+                            <File Id="fil0AB6CCC6384F6C6B1A85535F2E435452" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\SwitchDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD3F2F08057724AFA25E4EBF3514406A6" Guid="{CB2E0B76-1F9A-4B9B-904E-FD373A79E56C}">\r
+                            <File Id="fil06EEC5BCC6DCFD868503648FEB22C2E2" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\TabBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp5D978D2112EB628D93B78BBB779859CA" Guid="{2F43967B-733D-4C8C-A715-175288FDA3FD}">\r
+                            <File Id="fil23FF41EA1352977BF0D165A14A816D7F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\TabButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6800D8FB1FBB983412E92B80451A1109" Guid="{8109FFCD-D535-4EAD-BA10-8A16D8409AA0}">\r
+                            <File Id="fil9E0F7A4390203BCAA10D42088F80E582" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\TextArea.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp71287354FF8DCC602BB0D3D414E05962" Guid="{71FB3A16-2141-4948-89C3-73AB0B166A7E}">\r
+                            <File Id="fil220878B77DD5BA3F56A6D5C7A5EAD1EF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\TextField.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0DF5FCC7255520F9D24CAC298FA859D0" Guid="{ADFEC3CF-FD46-4295-96F6-30FDEBBFAB2A}">\r
+                            <File Id="fil87EA3DC0E8CE7A5D1B6D1980848D1E5C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ToolBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp3301D802CB29561A15AE015C15DD8543" Guid="{0CEB23DC-F5E8-403F-BFC6-02DAEC745718}">\r
+                            <File Id="fil5D5982165FA435D4FA833ABC64663AF4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ToolButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp00A4060669FB650FE92DDD9A09A857C0" Guid="{F095E727-9F5F-486D-B30C-406D1CE193FD}">\r
+                            <File Id="filD17CC03AD31A7AC8639971FE0D6C45A4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ToolSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp2D3FB42E796C744949D0480EE5C04892" Guid="{211FA71C-4714-49D9-AFFA-CA730B964213}">\r
+                            <File Id="filAA9B4A8954E641AD01238DA365D35C84" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ToolTip.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp11B5D7ADB851C95DCFE5C71959222497" Guid="{6B13434F-4A6A-4874-95A6-C796BE931A62}">\r
+                            <File Id="filA16915B6759149D53E63B3A6662327D5" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Tumbler.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp27BC211BA8D2261D578FC390503266AF" Guid="{30C94813-4571-479E-825B-10EDE44E2077}">\r
+                            <File Id="fil64AED8EAC8A4A27B2290235AC66A8987" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\VerticalHeaderView.qml" />\r
+                        </Component>\r
+                    </Directory>\r
+                    <Directory Id="dir98C439A324E49AE59FF9F7CAFC527818" Name="Material">\r
+                        <Component Id="cmp60AB973400004A31DBB4967567BB3780" Guid="{7D351719-339C-46E2-81CB-905AACB7BEC7}">\r
+                            <File Id="fil64C921AC09AAAAFB16510D0E258259FF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ApplicationWindow.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp026D50205EC5D03323420AC1EC10FD89" Guid="{4B779DF7-4593-4A4D-AC2D-38DC4BF88B0B}">\r
+                            <File Id="filDB01E686286D4C2DEE2CFA3FD38FCB40" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\BoxShadow.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpE0BD5867C82F9D8554FECDDC4B6901C7" Guid="{852AD272-C64B-4C36-96B7-8664046E31AA}">\r
+                            <File Id="filE4A4C7B299CDCB670C96418B8EDDB5D8" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\BusyIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp9F4E4D4D0534820ED7B1592321BACF51" Guid="{A6935D04-49FA-49DF-82EC-BE62493947D5}">\r
+                            <File Id="fil01470810824935FDCE6313DD60621BF3" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Button.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpCB85EA75A98576ACBC265DECF4F7697F" Guid="{12C6E0D2-3BFA-4BC6-8111-800F3AB9059D}">\r
+                            <File Id="fil1D098E5C4AA1CEC913EAFA6AEC902AB9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\CheckBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD8B043EAF161B1EED3C0D0E0E8B0717E" Guid="{C800976E-3768-42C1-9BE6-CC3B47528DC8}">\r
+                            <File Id="fil4E240E0B681A24079C96E7EE1B59BB81" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\CheckDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp006E05B629020152BA06266DCF586F94" Guid="{BCED27FF-F076-445C-A572-6A64493517E1}">\r
+                            <File Id="filB14F8F42CDE72A0055838F894912C34C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\CheckIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6390600916A9D80DEECC6B64C1A5734B" Guid="{A3D8818D-3D8F-4412-A219-82CAD7EA3A78}">\r
+                            <File Id="filAA1D779F78FFD687244475818E1E676D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ComboBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpA029FDE3C079115EA43947112C138B8C" Guid="{79E6AF29-37B7-4067-89C7-C733024B28CF}">\r
+                            <File Id="filA865175498A738900D1E48E261E9A7A2" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\CursorDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp050C776C9A28886790788280CCEC3D91" Guid="{E6A61937-0614-478F-A2AE-986106471D6F}">\r
+                            <File Id="fil3A782A789161DA1643DAB87A06031CC9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\DelayButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpBFBC5C595627F87399C1078F9A522E9D" Guid="{B0F3C31E-D1FD-4C88-9603-9DC8E7359565}">\r
+                            <File Id="filAA2365440AA7A931BCC671A9A5ED2CC3" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Dial.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp2B0856221A67BDF81C2238000B184A17" Guid="{F1026998-7BAD-4314-85CA-4B879412E77F}">\r
+                            <File Id="fil633492C397BA579319833DD3AAD2951E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Dialog.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp835605203F95C84205B718EDDDE2091E" Guid="{CC7B5354-0ED3-4872-9481-CD24218471F3}">\r
+                            <File Id="filFEB37EE6FA694858E6397E30CA14D53E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\DialogButtonBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD37BF99FBDB8EFD807188027A80B8DA3" Guid="{1C7EC37F-ED5A-4AF4-824B-78897568843C}">\r
+                            <File Id="fil9275FC5F45942812B0181C0AD249DA8D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Drawer.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp31DA9547A00549AAE408F3DADB011332" Guid="{F1E700ED-73BD-4B51-8D93-13745C69E2DC}">\r
+                            <File Id="filEE7349C912899BD32A59C5B164F24EEB" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ElevationEffect.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpC869C8092B8F2AAC631C286328891A94" Guid="{FDCDB596-6781-4C12-AD6E-BA12D276162E}">\r
+                            <File Id="filD25404A21A1FB54078D4A8BDD39139B9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Frame.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0305CAFC1FCE93ED480CC829AAD832AF" Guid="{A75B3F5F-8C84-4A5A-9D6C-B2D06223F0D5}">\r
+                            <File Id="filAFE4512E9BFCE1DBEA4D23E504D69326" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\GroupBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp5D66D11EA79AD27A1AE641BF9C7CB9F2" Guid="{3CB05274-E6ED-4762-B185-AAD551464977}">\r
+                            <File Id="fil2F55E2A2B3B694DC0B8F749076B915FC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\HorizontalHeaderView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpA817DF6EA38B1DF29BCE614C5450544B" Guid="{90C497D8-B166-45CF-A710-1DD2C5BFFA41}">\r
+                            <File Id="fil6DE65D9A2CA853FB7F9873AC257E7BF1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ItemDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpFB26590AF40355E4D807DA7009B7836D" Guid="{F9B22F54-D0AD-471D-B1EB-96E584134B22}">\r
+                            <File Id="fil677DE17071787C3A48E9C6FBAA519F5B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Label.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpFA168D7F4EFC52948B17DF862E07E25A" Guid="{C5F7BEEF-1B71-4D45-B859-4CA51121EA22}">\r
+                            <File Id="filEA122269BF1F9F0E5DE2853767AC894F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Menu.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp80B129BEE9AED15B717DBBC4E1C91B8D" Guid="{E64751DA-EE5A-4C99-9AD0-24FCDAA4D5CB}">\r
+                            <File Id="fil16F020BBAFC0D9DF9F1A85E68CE222C1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\MenuBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp222BD8179F2D4ECA9CD649598F1665FE" Guid="{B3D9F018-4A95-4520-911A-84862B132195}">\r
+                            <File Id="fil0BBF38951FC82CBE8636981293A2B84A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\MenuBarItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpE9263BDE73548B76D4CF00FA421C6394" Guid="{00E8C1F0-E2DF-43CD-B437-E781866A6E5E}">\r
+                            <File Id="fil12E8E206331572B0EC4AD0A4EE328DC8" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\MenuItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp665380529EE1C0ED1DE23E8B26850B19" Guid="{85D9EC2F-DABD-4B96-8B43-107FEC2268E8}">\r
+                            <File Id="fil3BCAD81F81A42FD7A740540891FC5A4B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\MenuSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8DD36362549E19ECEA88E3D1B1ACBB85" Guid="{44592660-851B-47BE-AEFA-34A0EC7F3289}">\r
+                            <File Id="fil615CC9ED7E0F011723181A7F99A61740" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Page.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD2EA9700B16C8836D5206D1185C549A0" Guid="{35B14087-6829-4BA2-97DE-4EB8F160A07E}">\r
+                            <File Id="filAD99EB039065C97EF184865D6503E4C1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\PageIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp73D2F27C7DDDD4CF7ADAEBF8E6E42EB1" Guid="{2EBCA9CE-E074-4C0E-BDA0-D9CFE4AA041B}">\r
+                            <File Id="filC2DFBF2CB10C8FCB3D1AF32F2A8D57DD" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Pane.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp7E31792D0EB10DD8AFD37842DFFCA924" Guid="{8A30E34E-392A-406A-8DCA-131248202236}">\r
+                            <File Id="fil1C6676EE896DF9A856E5FE3F9EFAC32C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\plugins.qmltypes" />\r
+                        </Component>\r
+                        <Component Id="cmpA158D23F2833A86832CC8AB04B51E8D0" Guid="{67B192AC-1AB5-4AD6-BFF4-71902B569AD2}">\r
+                            <File Id="fil70BCFDA53E4BE604641DD96490B3F5EE" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Popup.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpA62BD30E70FBA79DED1CC038B0404EBC" Guid="{F121CC1F-A122-4191-88EC-836831DD39E9}">\r
+                            <File Id="fil900BC61782FD456E2C7E15DCBA8CAC47" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ProgressBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp78F57F7853BF2F83CA4C27E09AE1E09D" Guid="{5200AA4B-3F84-4020-B572-61028451737C}">\r
+                            <File Id="fil95B30068F5A41AD2EC7F7D95D514FB65" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\qmldir" />\r
+                        </Component>\r
+                        <Component Id="cmpE3E3B7C94E5AFDAF94C8CC78998771DF" Guid="{27D226AC-CC01-4B3C-9CD9-FF83E2BE77C6}">\r
+                            <File Id="fil4C5BC6926C010F47CFB93190824A9A38" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\qtquickcontrols2materialstyleplugin.dll" />\r
+                        </Component>\r
+                        <Component Id="cmpBC000241C708266C86D302A435E66F1C" Guid="{5A4DA7CA-1BF1-4394-A52B-61834B951EC8}">\r
+                            <File Id="filA32CA6F1B47518CE771D5FD69B4BAE80" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\RadioButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp3B8600643B709AD9AD113079EDB50C94" Guid="{B6B8BD52-4464-46A3-8A15-2D9DB2F451FA}">\r
+                            <File Id="filFDFEFD51A9E2DF0C75D2E48D4697846D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\RadioDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB755C164EDFD73E1498268DB3DA880E5" Guid="{EEC2C85F-94A4-463C-99D2-0C94DDAB1731}">\r
+                            <File Id="fil438F051667D9B5E1DFEC7C42714E7AC5" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\RadioIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp1CBDAA564E02886155300F7A8AF049CF" Guid="{52FE64E7-76EA-4E72-8ABC-F72A6912CEDD}">\r
+                            <File Id="filEFF497CDD0E8BF7A1A284FE00DBAC232" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\RangeSlider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpFAE3866F17FF7039CA9618739C64A1A5" Guid="{1D8BBF80-37C6-4356-981C-E84624F4D130}">\r
+                            <File Id="fil950F62018D52CBBA67D0D037CE077CAF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\RectangularGlow.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp1BEDE14E6357E29206EDC740AE50650E" Guid="{3832C565-CF30-48A8-9F57-1F1203A9D094}">\r
+                            <File Id="fil7B1D1957A48A219B6EA1E51D1B9D5094" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\RoundButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp9848706507698E3D2DE99B0C079EAA41" Guid="{F4159B1A-5628-4239-B834-139C4300EE69}">\r
+                            <File Id="fil7B7E8B589CFA5A08CD0BDCFEBBB37F6E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ScrollBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpAF75487C0AF2238D5F5AB27EE11B8C29" Guid="{CC851481-1F5C-40DF-B542-CF2331CFBDE2}">\r
+                            <File Id="fil6C7298CEC9325745BABD8553BD2C314E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ScrollIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF1D853F627859C5B31EEDA150FC58C6E" Guid="{1CCA22C1-4843-4405-84E6-1359524F55C0}">\r
+                            <File Id="filE2D55223B3943A1418684BC06ADC9873" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Slider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp24C324DDA2757C24A5D64657F10D1861" Guid="{036A1520-EE7A-49D0-AB3B-6FDA0821BF57}">\r
+                            <File Id="fil456E349BA3AA06452991F85C418012A0" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SliderHandle.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp24A094BA67CBF70909850C9D82D581FB" Guid="{D516F3DC-29FF-4823-9F3C-2F4DDC120705}">\r
+                            <File Id="filD62846BDB9F588D73844FBE505CDA20B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SpinBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp855A7D3DC1E9E7D07C156736FB529CB6" Guid="{322DCB01-BEB8-423B-B3CF-9260650724A0}">\r
+                            <File Id="filB7F5B7769F99CFB3EEF067B579C69441" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SplitView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF44D1A83846E86F2D4EFFEDC9DA14661" Guid="{A0B9F909-8889-4C14-BD04-3250095E9F92}">\r
+                            <File Id="fil2E77DB68C32D27DC45D541C2515A7DCB" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\StackView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp15902266961B205BB7C49E3F21B0A166" Guid="{D1B88C45-21E8-4C9F-96FF-EDE6CBCB383F}">\r
+                            <File Id="fil6E8DF910D77FBD6DC889D4B17EDB8C73" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SwipeDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp50966487B32C316D078827563E31DB30" Guid="{F008F394-1A7C-4F15-ADE3-4BAFF638F1AB}">\r
+                            <File Id="fil04105F1624C2D26E2FF0F79EEDB6FB6F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SwipeView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpA1B9598A2B3B9C203B5780C91F7075D7" Guid="{A0B5114E-0A0C-44EF-BE1D-C9CB730A4EAA}">\r
+                            <File Id="fil8BD5245958D87E18B746A319B5EA03EB" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Switch.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpEF587C49FDE4B67961EDEA007128D3DE" Guid="{FBEE1A46-7848-4E35-9344-6D6B062DD6A5}">\r
+                            <File Id="filE64764B9E04E5C2C205F98B593CE79AD" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SwitchDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp793DBF048FBD4343297F1E17756BBF6B" Guid="{8185D55C-9168-4D19-BEAC-34505A4D7106}">\r
+                            <File Id="fil7BF94FDDFE952066A6BF1FC7268E4C41" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SwitchIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD98F61FC599E1BD66057249B28016A0E" Guid="{254D4BDB-E170-408F-B943-DFF5044C0519}">\r
+                            <File Id="fil35E18FE45A8EF14EF0AF02F9E3BB944D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\TabBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp4C6EA3AFEB0324ED3875A46DCCB4EA48" Guid="{48E4E179-8433-4CED-A5C0-9A31EEE63415}">\r
+                            <File Id="filAA08F0B10CF6925991C628327DC46BB4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\TabButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpA7F194C9D57330A5F72C74CD96507E48" Guid="{576229B2-14D1-4DAC-8482-31499B873B5E}">\r
+                            <File Id="fil5669F09C2045004CC25FD46B2565629E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\TextArea.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp337BDFFE231941F8A3E54BF4AC644B58" Guid="{F791A188-A943-454F-AEE1-FD797A3D801F}">\r
+                            <File Id="filE58155B28FABF154CD98B2C02EC12126" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\TextField.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0255A607D5334E94D517A2FEDDB2105D" Guid="{10D77816-5456-4FF9-B162-3DF69F8E56EA}">\r
+                            <File Id="fil9261EC814FA4325E3177494A396D8066" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ToolBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpBDEB6272B93E7E2FD1982012B5EF3D02" Guid="{7028EB68-4086-4009-BAD5-6D19E4D5EC9E}">\r
+                            <File Id="fil36BCCA29A63BD763D066F4FD320B6D05" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ToolButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6ACA30972AFD6D78448054BA4368AEE7" Guid="{8D2AD71A-31A2-4ED5-B3BD-43E761CB7142}">\r
+                            <File Id="fil21C810954616BBB037440CDD95167F6B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ToolSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0014F4AC9A48147F8EAB3F17B2C144E0" Guid="{DE7EB59D-A01E-453B-B333-887D374C00EB}">\r
+                            <File Id="filE2CD377318429F05238B017CA75F21B1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ToolTip.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB1ABCFD82FD1858E7657C34A8104404C" Guid="{8F3123BF-9523-47FF-ABE9-2B9B3F5D0181}">\r
+                            <File Id="fil98EBD20C2BD6799247161723C27818F9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Tumbler.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6A432A70B05CC4AC38DA38F776B97812" Guid="{4E8C826F-1AAA-4063-9CAE-38614D103CD6}">\r
+                            <File Id="filB345996D2977939507887C6FB257738B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\VerticalHeaderView.qml" />\r
+                        </Component>\r
+                    </Directory>\r
+                    <Directory Id="dir8677A4B366B4AB433BB4754EEBBBE2EA" Name="Universal">\r
+                        <Component Id="cmp134038F5FD44A08B4DA0CFD8921DFB42" Guid="{1B4AC1E8-E146-4872-A0B9-90AAF7A63406}">\r
+                            <File Id="filEE54942B1FE1F7E0FE0DA9372D63E2E9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ApplicationWindow.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp34FB0E37566676B69EEEB98F670D23C4" Guid="{09BBA204-8172-4676-B116-B7249D1FB914}">\r
+                            <File Id="filF048A4DBF3EE1F42AB0045720B3A1275" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\BusyIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp166A7C72CA2D82533EBF24FB87FE4BC5" Guid="{5D75BCE9-3E98-4292-BC43-C72DA6C7B3FD}">\r
+                            <File Id="fil9FC3621AAA61D06486256ACFCEDEFA58" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Button.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp11D7406D35C2641BC20018F1FA8ADC45" Guid="{50AC31B7-B2E4-492F-97B4-7125DFA06DCE}">\r
+                            <File Id="fil4CC941157F98FD9DCC1CD03DB1383D81" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\CheckBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6CA80FCA9CDD4C55F2582BF139AA4B1B" Guid="{C9EF0708-5E19-48B7-BD71-120ACD0FA185}">\r
+                            <File Id="filAA2312432C29EB3BE6C44D5CB9E552C0" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\CheckDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDFBFE88B708CE2CA5904473FE0CDB861" Guid="{0135FF85-D1FF-4305-9E95-CDFA4427F376}">\r
+                            <File Id="fil0D3BF90DE4E4AC4108503B2DB286E7F6" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\CheckIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8D47A9ABEDE81A100C924FF154224483" Guid="{AD996097-10A0-48E2-AE3F-2A31A8B99BC7}">\r
+                            <File Id="fil3445161ADB5A36CCA4D8DEE8401F79D9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ComboBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0071C7F9D6D4B5DC3218D487DABFC218" Guid="{85A7872B-F79B-43E1-97CC-B30383C2846A}">\r
+                            <File Id="fil1B3A6163FC6D639F2BD22118F8DA406F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\DelayButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp54B17CC907A926962454D9FDEA99E369" Guid="{4492FD9C-0227-4592-931B-1CE255A56335}">\r
+                            <File Id="fil1E9230BCC1B46DE57D1B254EB036939B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Dial.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDF321BF03AC6D81897FFD4B64045DE88" Guid="{76EBA34B-E864-4B0F-9382-2786ABA98828}">\r
+                            <File Id="fil64F4012F0ABC224742C0420695795D29" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Dialog.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpEBBBCD3B40ADAF40F5F7BBDAD1628BAB" Guid="{3CEB3BEF-B036-491B-8156-71DFCF99AEEA}">\r
+                            <File Id="fil1AD8E7E159B82A10473C11ACD1622A84" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\DialogButtonBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp4781FFA691E6B55670D19CCA10E68D4A" Guid="{D15EEBBD-53D6-4922-AA40-39A8594785FA}">\r
+                            <File Id="fil4E217CB94A8B78C9D074F1BAEBB7A41C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Drawer.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp9D75099F72BC1EA408C69BAE1036713A" Guid="{5F48BA1E-8F54-465F-8738-CD28295DD983}">\r
+                            <File Id="fil9D815A989044908A0CB2481100231444" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Frame.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD507403E3878D92F029FB454CD349781" Guid="{A5180010-062C-494E-82C5-2A7804D7118A}">\r
+                            <File Id="filFA88E95F2ACC7A5522DCF4C107B83B7A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\GroupBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDB50F59C1041F3C78FB11F782D92A59C" Guid="{DC08DC21-9312-4C4D-BF3A-C43E1A675FB1}">\r
+                            <File Id="fil911A2600E1379E8C5A85199969DB74CF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\HorizontalHeaderView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp9ACED442699FD1B84B592FD9A678FF1F" Guid="{9D19D01E-C44E-4FC6-AAD5-6709B0298D2B}">\r
+                            <File Id="filB5F9B6495241535B1200DA731AF5A5F9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ItemDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpC959D8B9C731BB4CB168C53232754164" Guid="{AE31E225-63D5-46EC-BFF9-01D536DE1235}">\r
+                            <File Id="filE924AE836BA5C65F25B0FF78B6A2425C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Label.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF036082D9E70D219B49993BF2078FC9B" Guid="{740E5BA2-9846-417D-81D1-C4F6AB3049A7}">\r
+                            <File Id="fil0FE051A3F7254F4C87AB2787BB8B9195" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Menu.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp610C62A18FB0CBD4A23E6420089991A6" Guid="{D53BB533-7392-4096-8630-CA6D234AC952}">\r
+                            <File Id="fil0078083DF303E6033ED65F84CCCDA8E0" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\MenuBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF0A89D556198AEC41AA931DE764420C9" Guid="{2478D487-C1B4-498E-A15E-8AFC8EBDD54F}">\r
+                            <File Id="fil141BCF473057F39197B9662FCF606CC8" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\MenuBarItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp1FB4C766D7DFBDC29927513C52FF444C" Guid="{07AC968A-CAD2-4435-B348-325CF309AF6D}">\r
+                            <File Id="fil1E9D45ADE39F5FB611AB5AAB33294D3D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\MenuItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpAB3EF735C77E4B6501B74C3070F17E8D" Guid="{9C5FC5C2-834C-48C0-980E-2D5179A53773}">\r
+                            <File Id="filD6ADE8C35F973E505931BD4F09EF0A78" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\MenuSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpEF370FB1E0BE2F7C44ACA77A0B9A380F" Guid="{FEE2D791-0609-42CC-9183-81B0FCFB3AAF}">\r
+                            <File Id="filBEEE5C63314DE5A1C43E6865925031F3" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Page.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD53F72AC172BCE3F947CB966768B983E" Guid="{55F0B5A9-6568-4369-BEC8-9CE9FAB4C886}">\r
+                            <File Id="filD257FB636047A1ABDD8322D56BF83DED" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\PageIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB58DE0208168537FC63A28BA40094D3E" Guid="{DDA681AA-E7A3-4935-B6F3-A26C591C0F3D}">\r
+                            <File Id="filF940749E1F1250D0E5E454F677E4811A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Pane.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp1E24968394165967A4C933056CC97E8B" Guid="{BCC5C83A-1795-4E9A-9F3B-9EF43B56B4E2}">\r
+                            <File Id="filBBCB3725136BF248504E6130E398CC37" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\plugins.qmltypes" />\r
+                        </Component>\r
+                        <Component Id="cmpA780D5122E140F762FC4C3A0D02E7179" Guid="{8898C5E7-1BF7-40C3-A46D-F097D6A2110A}">\r
+                            <File Id="fil9FC53D09D020635EA1B1AC7DAB106FEB" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Popup.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpE61937584021054E6A0A34AFBB281937" Guid="{6937E766-8BD5-415D-B76A-F464FEF6E71B}">\r
+                            <File Id="filA8BB46F9B5116CF916C7B675148EFC4E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ProgressBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB2D199D19AF00559ED6198A864F4874E" Guid="{DED65452-7CC9-485D-807E-43904CBA7FEA}">\r
+                            <File Id="filFB7A404E13EF7802DE1DDCBEBDD42A2E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\qmldir" />\r
+                        </Component>\r
+                        <Component Id="cmpA29A335EDA4A585067146163B51D3643" Guid="{86C75BC9-682C-49B2-A48D-B18AA904AAF9}">\r
+                            <File Id="fil23BEBDFB38598BCC408151AC6B584604" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\qtquickcontrols2universalstyleplugin.dll" />\r
+                        </Component>\r
+                        <Component Id="cmp4F8C12B67EDC519F78AA4840C85494D1" Guid="{4F5352F8-AA69-45F8-B461-8FB3CCEDA086}">\r
+                            <File Id="fil2FF1D14D6D70E386EAEAFC412E469DBA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\RadioButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp30F05444D83E8DAFE75CAAB16F50E3AD" Guid="{A4922AB5-4A0B-4CDE-A60E-6B88921844AB}">\r
+                            <File Id="fil5904A6F1649189A762EE1680E07001FA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\RadioDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp7573B19F4A56CF2B1D359A87D25A39B4" Guid="{2B71C5A1-2DEA-42CF-B0B5-8856B0B47752}">\r
+                            <File Id="fil0FDD8C1A5AF94ACD8822A21F4762C124" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\RadioIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0B698F624CC72E780FB1FCCBE6340AAD" Guid="{F91E47DD-8607-4DBE-938B-D67E14187DD0}">\r
+                            <File Id="fil8EB7C1B7193D2DDA9E532A1B16502A73" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\RangeSlider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp898F4E8E360375D6D8FE6F606F899441" Guid="{EF555FB1-D8BD-416A-B581-1DCCA58FAABD}">\r
+                            <File Id="fil8455667A31A0B1322CE00154CCF24A08" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\RoundButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp246EDCD16E1C5BC88F69EFDF059C62B6" Guid="{363B06E4-66EB-437D-9454-16D14379D4A1}">\r
+                            <File Id="fil8161E8E97F7F2526D40A4E9426ED9CAC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ScrollBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB92657C823E717DB4819343891A10BB1" Guid="{3860F820-ABC8-4AA4-A7F9-8B2D10A85511}">\r
+                            <File Id="filF04EF2D65266F90D588563833EE46211" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ScrollIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF4DE6E8F14E6A8E27857D8BC89787BAF" Guid="{DE05151F-0839-4D83-9AB5-7D089240403F}">\r
+                            <File Id="fil4987E569AB3CD95456311C90D8DB245D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Slider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF49A6AC910C2EB6FF0917041E1F96E8A" Guid="{74BB77A1-0A70-4175-84CD-C4266DDF3112}">\r
+                            <File Id="filE8E232330635AC59E8BE3A0694C54370" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\SpinBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp1DF54EA158668A0E21013A820D2E7F51" Guid="{56BF8F73-ECAC-4474-BAB8-CDC47BC0A267}">\r
+                            <File Id="filADDD2AFDAE9A339C8A92C247781CEF40" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\SplitView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDFA6653A3AA375D59BE8318C927D5DC4" Guid="{B734122F-CD08-4401-B7BA-5E1D5858EE5A}">\r
+                            <File Id="filAC7F2D3C36D8E02F2FAF7A5578199576" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\StackView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp20B23CB93B2BC9BF69A97D0994698310" Guid="{E5A49C0D-CAA9-4E51-A610-C46AF31FC864}">\r
+                            <File Id="fil5DFC169D4B5F10EDE586C1C5760B6573" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\SwipeDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp735A0D9DB2D96E80F3352A232FCAB063" Guid="{E77595FB-FEE6-48C0-A749-862C8CEEC8A0}">\r
+                            <File Id="fil0867DDC68FB2A26C4732AA7C6CF0EF5F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Switch.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDB346892873A00B4024D345FE4C725BA" Guid="{43E075FC-DDDD-43B2-9308-F8174880A344}">\r
+                            <File Id="fil83DE4DE33E72D0F08A1C59200E82B424" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\SwitchDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp32E0784BA82190D0BD64E488EF265F55" Guid="{EEB2DEA6-7C4F-4CCB-9474-1E5F83FBEEA2}">\r
+                            <File Id="fil26EA260B1D46E42906FBF1BB1CE3B145" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\SwitchIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp4215773DF3A614E82EF59C456A6EFA97" Guid="{EBFB2786-8DCA-4B6B-870D-4FC90BD82E23}">\r
+                            <File Id="filA8F80D2C80356344CDCD864FCA88D48E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\TabBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp72E1D5A67C90BF0538F8015E486898B0" Guid="{BA27BD86-15CA-47C1-8D6A-7998EDE35547}">\r
+                            <File Id="filDF1DA27F8944A4FC08060F351077F7E8" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\TabButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp02A0C8EDA0860CBAE68AE51FAE93EBB1" Guid="{3840413B-B9C9-4DD5-8AD5-D03695BC18C2}">\r
+                            <File Id="fil039EFA8518F7A118F885C10615BAE8BA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\TextArea.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpC54A96A34D6E8EEB099E5754699E907D" Guid="{14E4461E-D756-4E2D-A569-2AB17F5BD204}">\r
+                            <File Id="filF7DE49C3E03DBE7D7DAEF87FB2CF7EEC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\TextField.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp2564368EA9BDCD6CBD86BD0B47845CE8" Guid="{D705AE33-5B56-4582-B520-1967673B5A01}">\r
+                            <File Id="fil7BB9016C1239CAC2A691B7E80E155716" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ToolBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp4C037A4C1F7E6103342D757CE84FB3B0" Guid="{37196B4C-A886-40EC-B577-360811B5BBA2}">\r
+                            <File Id="filE5A4593700B13AAF5E707064E4DCBD71" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ToolButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp2782B54139300C706E3DF5D779A32A4E" Guid="{2096E3B3-6475-456F-8104-7044EE32C396}">\r
+                            <File Id="fil043B63F16633A9DEDFD28882B52AD567" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ToolSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp5ACAEC60F8EC208DC179E4790FE9090B" Guid="{29854CE8-EAC3-4341-BBF3-6A65CB8B1E5D}">\r
+                            <File Id="fil893C89D4CD3775E4FDF5EBC7DC2F2B64" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ToolTip.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpE672C34FD6F4C63A61786B77A67D0BCA" Guid="{317CDB5D-8CB9-4E8A-B272-7EF4044501C0}">\r
+                            <File Id="fil7F438968C07C1FD861B594449C04FB30" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Tumbler.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp05B02931195D1A41BF37EA6B3C26F10B" Guid="{D6329835-9FFD-46EB-8BC2-7E253275209A}">\r
+                            <File Id="fil147142765918ADD0B8E6F1C083AC829F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\VerticalHeaderView.qml" />\r
+                        </Component>\r
+                    </Directory>\r
+                </Directory>\r
+                <Directory Id="dir05FA62F47E8C43CCE7A3869D5CB62C6D" Name="Templates.2">\r
+                    <Component Id="cmp2214585F3508B759DB1308CC180757FB" Guid="{B480F13B-F2EF-46A5-839E-E43E5F688DD2}">\r
+                        <File Id="filAB37F3DFF1567C85745F8546C34DB8E4" KeyPath="yes" Source="SourceDir\QtQuick\Templates.2\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmpA1474840D69304F1AD34009D58C2B5DA" Guid="{38B68372-B6BE-4B9D-9A2D-757FEDFA3F74}">\r
+                        <File Id="fil58857EAF612484AD6F1B7594F5496CA3" KeyPath="yes" Source="SourceDir\QtQuick\Templates.2\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmp40B743148ECA12B22598A157C6147B6E" Guid="{9450D1E0-9549-4179-9BE3-303787AE3E0D}">\r
+                        <File Id="fil3A4423AFDA5CD5B5725F48FA3C3D437A" KeyPath="yes" Source="SourceDir\QtQuick\Templates.2\qtquicktemplates2plugin.dll" />\r
+                    </Component>\r
+                </Directory>\r
+                <Directory Id="dirA3E21ED94D77D9B21B5BA9468AEE4AB0" Name="Window.2">\r
+                    <Component Id="cmp104D8EDE2CF475C5EA4FE8169E74E299" Guid="{C251DAA6-2564-4B1B-AF03-E008227A46D3}">\r
+                        <File Id="fil0DA074889234E8261DE830FC8ADD1A9B" KeyPath="yes" Source="SourceDir\QtQuick\Window.2\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmpDC8A03C1B37F2CFE043AED01502A741C" Guid="{D5570982-F17E-42B8-A5C7-BDE01B78A561}">\r
+                        <File Id="fil06DE355E21B919381A7CB928407CB593" KeyPath="yes" Source="SourceDir\QtQuick\Window.2\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmp87D7B0E5B5E51059D964EDCED844F4AD" Guid="{45586400-217C-4389-AE8E-89EA2A8FF29C}">\r
+                        <File Id="filAFCC613AFB897A177641333EC6418959" KeyPath="yes" Source="SourceDir\QtQuick\Window.2\windowplugin.dll" />\r
+                    </Component>\r
+                </Directory>\r
+            </Directory>\r
+            <Directory Id="dir00FAA9E14E20D0621E9EF928F4E50AD3" Name="QtQuick.2">\r
+                <Component Id="cmp80E63D0ACF59FD23F5B051FE5BD003DF" Guid="{B7FA0594-A58C-4959-B5A0-F550B3224D62}">\r
+                    <File Id="fil71AA2CD6AFACEC9523574269EB6E2F80" KeyPath="yes" Source="SourceDir\QtQuick.2\plugins.qmltypes" />\r
+                </Component>\r
+                <Component Id="cmp65C97B5A564AB844DB2F02FFA5B163A4" Guid="{9EA96BC5-BDB5-449F-A6F7-BB9AD9A146F0}">\r
+                    <File Id="fil5B9ECA215F124EDEB5921629049ECBD9" KeyPath="yes" Source="SourceDir\QtQuick.2\qmldir" />\r
+                </Component>\r
+                <Component Id="cmp4A028D7062688CA7458F90092B47AAAC" Guid="{AB143E12-669C-4FC2-A85D-4A67112D8041}">\r
+                    <File Id="fil4ED5B7A970D8897099777F501E3BDC88" KeyPath="yes" Source="SourceDir\QtQuick.2\qtquick2plugin.dll" />\r
+                </Component>\r
+            </Directory>\r
 <?endif?>\r
         </DirectoryRef>\r
     </Fragment>\r
             <ComponentRef Id="cmpC3CB042567F53DD612D309E9CA2E10A6" />\r
             <ComponentRef Id="cmp43D57F897A909679308EFC03849D7E2E" />\r
             <ComponentRef Id="cmpF5EA7E37332E95C614BD2D7EAA933E5D" />\r
+            <ComponentRef Id="cmp8DAF13208A863A841AEE7A46E79FB87C" />\r
             <ComponentRef Id="cmpA214B663A372012F5DCBA6A5CDAE421D" />\r
             <ComponentRef Id="cmpFDB70117D8E46EB188997470A3793B5D" />\r
+<?endif?>\r
+<?ifdef vs?>\r
+            <ComponentRef Id="cmp26187F331810660B2B23AB9501BE69AF" />\r
+            <ComponentRef Id="cmp10C81658E3C8F63B19EF9D1C26B58363" />\r
+            <ComponentRef Id="cmp5B4749C387D33EAF993DDC89F9DBC102" />\r
+            <ComponentRef Id="cmp7F83581B3F7835094D371708381AD391" />\r
+            <ComponentRef Id="cmp33B78CE22F0CBCA4F41C5037A792210C" />\r
+            <ComponentRef Id="cmpB5571DB4C3153CA848ED1238DBCD4281" />\r
+            <ComponentRef Id="cmp4C1BE3EACCCE195C8B88434D5DF77DE5" />\r
+            <ComponentRef Id="cmpE4BE73E4CCE2EE3CCA22CAE0C82CD298" />\r
+            <ComponentRef Id="cmpFEC2C3E75BBEACDB1A6F4AFB808CD58E" />\r
+            <ComponentRef Id="cmp8B3D94901F4D369CD276B339B8B00F70" />\r
+            <ComponentRef Id="cmp0B6C2EAABA7EAA14C510F4C9D68A3F40" />\r
+            <ComponentRef Id="cmpE75E4FEE65DCA51D044DF4B2AEDCD828" />\r
+            <ComponentRef Id="cmpA8BBE7010AD624EFBA7166925167402F" />\r
+            <ComponentRef Id="cmp00B1DB73ED33EC1DD6658036D3901CB7" />\r
+            <ComponentRef Id="cmp9F481CFBAECB8853AF07C1D5060B77D7" />\r
+            <ComponentRef Id="cmp93816105EAC51740B98AF9C1041D28D1" />\r
+            <ComponentRef Id="cmp5ED50822278A4A846E891B41C07F7B6A" />\r
+            <ComponentRef Id="cmpACA0A17DA54991A194B823CE40D06263" />\r
+            <ComponentRef Id="cmp3D2C980EC975DBBCBD13A9178DD1A03B" />\r
+            <ComponentRef Id="cmp356189FC3558D542AA8AD79067A42632" />\r
+            <ComponentRef Id="cmpABC392DBCC9A5A469B77C129899873EE" />\r
+            <ComponentRef Id="cmp740F889D5799A2FF0F93A24058617EB6" />\r
+            <ComponentRef Id="cmp2799826256C0E84847E94E11C7878018" />\r
+            <ComponentRef Id="cmp659424F663F96AE55FBA532B4CBB2EC8" />\r
+            <ComponentRef Id="cmpDF6A99C1FA6B3943D690F21136BA7F9F" />\r
+            <ComponentRef Id="cmp81B459304782BB6AC65E381BA13726B0" />\r
+            <ComponentRef Id="cmp3CC0196A91D9C805D66BFFF4EA38EBE7" />\r
+            <ComponentRef Id="cmp67E65C02C04C3567636F47EFAA888D12" />\r
+            <ComponentRef Id="cmpADC8E894229BBB48BBC40D4F033FD791" />\r
+            <ComponentRef Id="cmp0D2D3E713559661141BC5DD9F687F29D" />\r
+            <ComponentRef Id="cmp3C102DD93EC83164DA87E7820364DDAC" />\r
+            <ComponentRef Id="cmp4B666BB592E998D07B55C82CF76F5B1C" />\r
+            <ComponentRef Id="cmp4C99F5024E504FEAB151291478878934" />\r
+            <ComponentRef Id="cmp84BC34E1E20AAE86B427C9CD3129C4FA" />\r
+            <ComponentRef Id="cmpCBA14389AA89D290E481E0F2625854CE" />\r
+            <ComponentRef Id="cmp6FFF646F52B60CE854DA0A1B8C47EC55" />\r
+            <ComponentRef Id="cmp5E04D231070BE2CE70B2672E56C6B9DE" />\r
+            <ComponentRef Id="cmp8C5CA067EC2AB41E7CAFE253F2EC86A8" />\r
+            <ComponentRef Id="cmp8441DAF2F2BC211F1F6FD10DCE7B51FA" />\r
+            <ComponentRef Id="cmpC3A1EC5089BA6B0798970A12E614BBDB" />\r
+            <ComponentRef Id="cmp854B1173EACBB218B6AF849A7E8A56D7" />\r
+            <ComponentRef Id="cmp7D04EA58DD5BF794EB2D3A75609DEFF9" />\r
+            <ComponentRef Id="cmp688C34D162C3029BDE9A236F29106F11" />\r
+            <ComponentRef Id="cmp7F9ABA6F59701379E7E54AB2EF4B01EA" />\r
+            <ComponentRef Id="cmp46E93DFCA823AE21F1FD874F13C0A6CA" />\r
+            <ComponentRef Id="cmp2F1F9DE69090BFD2946A7621F2DA410E" />\r
+            <ComponentRef Id="cmpE650E3AF80C0073C2B52BC369D45B79C" />\r
+            <ComponentRef Id="cmpB62C6E9BF562754AAC86BB52A361231D" />\r
+            <ComponentRef Id="cmpE98977BCFBF844FDBCB5325BFC639084" />\r
+            <ComponentRef Id="cmpA11A92D9D7D10A9221DD05699B7B1B8C" />\r
+            <ComponentRef Id="cmpE9BBCB95E45E4F80549E5C111FA7A855" />\r
+            <ComponentRef Id="cmpA9B83EF4B33682EFEA153198F1C44255" />\r
+            <ComponentRef Id="cmp8960218D3E3E2D16BFEF14C48404B5D1" />\r
+            <ComponentRef Id="cmp4951B8E17EA0B3A6B265E2C0E893A50A" />\r
+            <ComponentRef Id="cmp48FE69666F780EE04FC507C3AF73AB4C" />\r
+            <ComponentRef Id="cmp2BD742974C0791D6BD1AE4F7E3646D99" />\r
+            <ComponentRef Id="cmp36A30368477DCD705088EDB144FAA0D0" />\r
+            <ComponentRef Id="cmpF8459C652CF5C20ED87F853C1ABE1752" />\r
+            <ComponentRef Id="cmpA1791452BCB3728CD63E88FCBF971D62" />\r
+            <ComponentRef Id="cmpCA0AF56CA87A52053FE61B6ECD156E35" />\r
+            <ComponentRef Id="cmp0F8FC23D5E3669050C7A79355797347B" />\r
+            <ComponentRef Id="cmpBC9523950CF52702CB1A519123F9E519" />\r
+            <ComponentRef Id="cmpE2FE4AD9E8750F9DCD6AD8C03FFD973F" />\r
+            <ComponentRef Id="cmp302F033AE251BD62E648805EFB1BB536" />\r
+            <ComponentRef Id="cmpF3F704D8F2236A0A62B980FA41D3CB5E" />\r
+            <ComponentRef Id="cmp00AC1A2BE52EA04246F38C5F0B22A9D0" />\r
+            <ComponentRef Id="cmp0A8C093EE03780F9F7823442F10A6C82" />\r
+            <ComponentRef Id="cmp44B6838E73CD47A3F0061ECCE2EB48D0" />\r
+            <ComponentRef Id="cmp57A7D8DDBA7FB780E38A3CF0C3DE50F6" />\r
+            <ComponentRef Id="cmp74FB488898EE30854670C634A6CA57CB" />\r
+            <ComponentRef Id="cmpFF41000DFB8364F307DEC40444A8ED49" />\r
+            <ComponentRef Id="cmpF79607E800F2F8BD75203694AEAEF46A" />\r
+            <ComponentRef Id="cmp1E5895F7DD6F9A8239FBB2DD8A187651" />\r
+            <ComponentRef Id="cmpBAB6384A9280EFB631A74D56E4849879" />\r
+            <ComponentRef Id="cmpE8348F57B7F7ED3E102061755EA59678" />\r
+            <ComponentRef Id="cmp789571C85C4C76BD48E9A471271B5128" />\r
+            <ComponentRef Id="cmp07E1D3988D9259288197F71CC0B0559A" />\r
+            <ComponentRef Id="cmp91EB04F6C5AFAF292A0BFCE3570DCBFE" />\r
+            <ComponentRef Id="cmpDA0DD041411FA5AC356FAD21EBB0BFCC" />\r
+            <ComponentRef Id="cmpC0C294F18C416B2F09D563A7D2F171C0" />\r
+            <ComponentRef Id="cmpBBC07C7AA9E1D56538F0EE5BC883A106" />\r
+            <ComponentRef Id="cmpA2AD6F575E441894DDF6301F935EF3E7" />\r
+            <ComponentRef Id="cmp346061900FCE515D6B32CFCD7971325C" />\r
+            <ComponentRef Id="cmpE5AC86A748B88CA71F59F050376D4BC8" />\r
+            <ComponentRef Id="cmp26A45023E109A672FAA78FE5C4F827F4" />\r
+            <ComponentRef Id="cmp92014B7FBE8B02881A6A53384BDCD1C4" />\r
+            <ComponentRef Id="cmp372134AB6EF24BAC7497CC47466F0727" />\r
+            <ComponentRef Id="cmp17FB129D73D5BCA38BEC406E5A7A9853" />\r
+            <ComponentRef Id="cmp59BA1197723375EE2B1A8FC0971407A8" />\r
+            <ComponentRef Id="cmp9076C49961F4F0F4F5B4F65EF0CE7925" />\r
+            <ComponentRef Id="cmpCBF4FE8555DE1D61528AF79EA3C5870F" />\r
+            <ComponentRef Id="cmp63B837CFDE5F5C8ADC92D6A80E367621" />\r
+            <ComponentRef Id="cmp73FC7C1B9421F67443A7CA5E03C6F59C" />\r
+            <ComponentRef Id="cmp24D33989EF8148F7E8E801168FA75D02" />\r
+            <ComponentRef Id="cmp781F94BE6B67AAB9F2DD4A6865F67043" />\r
+            <ComponentRef Id="cmpBF8FE1999E64BE53CD0FFE028F2BA8E5" />\r
+            <ComponentRef Id="cmp508D7E9DE20A057C80595708342CCD11" />\r
+            <ComponentRef Id="cmp814DB5A7174FAD3D492689B3CFFD4FC6" />\r
+            <ComponentRef Id="cmp9FCA0D048C5FEDEF35CFDCF4220D148D" />\r
+            <ComponentRef Id="cmp8302E43F7B461A53125D24EBAB2C8BBA" />\r
+            <ComponentRef Id="cmpDD4764A31C5966C382C9FADCF2747DAE" />\r
+            <ComponentRef Id="cmp4DC6229A880E85A8EE64B69A175EB7F8" />\r
+            <ComponentRef Id="cmpC3E4C72480B21745D1D55462CD128148" />\r
+            <ComponentRef Id="cmp9C2C5E7D9EAC8AD7E9FB8BA9B0147BAB" />\r
+            <ComponentRef Id="cmp66ADBF15C183C1EF05BA59EBF57DC04C" />\r
+            <ComponentRef Id="cmpB85DB993D3D58E717F4EC2A2A5AC4130" />\r
+            <ComponentRef Id="cmp93014FB27329F9718833E30A8DCE2F23" />\r
+            <ComponentRef Id="cmp9D0CC60826A95D19D52A3A08FB44155C" />\r
+            <ComponentRef Id="cmp06BD88C78B72DD375F0FD8DC37812A7A" />\r
+            <ComponentRef Id="cmp6577B61F7B803092F263CE9BD76D7B19" />\r
+            <ComponentRef Id="cmpD880E6EDC6A23CDFF94AB5E0A0018A10" />\r
+            <ComponentRef Id="cmp00D081BB0762FEC39933D1C2960FC831" />\r
+            <ComponentRef Id="cmpD69A2401CBD6D26E44BE1271C8905A24" />\r
+            <ComponentRef Id="cmp0B7DD63BE81BC1EA8DB09A99C12CB156" />\r
+            <ComponentRef Id="cmp1EC09C5C4D6CE477EAA2BE5A3798E1CC" />\r
+            <ComponentRef Id="cmp577EBA2061AD5D464AC5B629EF1A63DE" />\r
+            <ComponentRef Id="cmpFD9033FF82B141429428B759027C0ECB" />\r
+            <ComponentRef Id="cmpD9A7A9076E4844ACEB4385E651C6FC27" />\r
+            <ComponentRef Id="cmp952D38FA5543D46E8B0B6CAC867E6D19" />\r
+            <ComponentRef Id="cmp8360F4A2F8333FCC0A95F36461DAC0AE" />\r
+            <ComponentRef Id="cmpA8C48D6004583C73A28027E805ECF241" />\r
+            <ComponentRef Id="cmpF0F52E0AA644EE5B8A4212507A2A9632" />\r
+            <ComponentRef Id="cmp1F2A598B5F572A95F619F914ADFDFD01" />\r
+            <ComponentRef Id="cmp894CC6997FDC49D32993AF049D19661B" />\r
+            <ComponentRef Id="cmp45A86D8F1E2A0DB9085F1C386BE8C441" />\r
+            <ComponentRef Id="cmpD7F91C9BF1B37E7ECEFC803745680533" />\r
+            <ComponentRef Id="cmp6C7380F6F9D3432EF0FF68155BE5B7EA" />\r
+            <ComponentRef Id="cmp163F0198648660C0C4DED70616CD3A50" />\r
+            <ComponentRef Id="cmpF5CA7A4D3DB71A67C3E4BC87D9A553DE" />\r
+            <ComponentRef Id="cmp2599D94F81ED612C05479E9A8F58A189" />\r
+            <ComponentRef Id="cmp9557C3464EC921CAC1A43D77B20BA92D" />\r
+            <ComponentRef Id="cmpFDD06D370422F04997F37CDCF9077AAF" />\r
+            <ComponentRef Id="cmpC5C6ADF92BCA401BFD4AB6B8DF07ABE6" />\r
+            <ComponentRef Id="cmpC7F75CEAF1EB837B6998ED5DC6550404" />\r
+            <ComponentRef Id="cmp4E0B2D6970453AF152BA060236BD8B71" />\r
+            <ComponentRef Id="cmp3A32B8F13C49569A0A4AD1D11045BE3A" />\r
+            <ComponentRef Id="cmp6D11E1D44ADD3049CE735F324F7F4521" />\r
+            <ComponentRef Id="cmp6763D9C183B24CD6659B616DD72994DA" />\r
+            <ComponentRef Id="cmp0FEA3716333AE56EBB72D727432DFB0A" />\r
+            <ComponentRef Id="cmpF77FF7EB150083CBBB82946EC3F849C5" />\r
+            <ComponentRef Id="cmp8693A45A9517FDC707611CD9592AD887" />\r
+            <ComponentRef Id="cmp22009CC98E7BCD73739238DBAD70F5FF" />\r
+            <ComponentRef Id="cmp006248D9C946635CD2C0D21D711BAF97" />\r
+            <ComponentRef Id="cmp8F4980EC013CDADC9677179C1B5FB4DB" />\r
+            <ComponentRef Id="cmp7CB5E11B511D00866734F2A59242071E" />\r
+            <ComponentRef Id="cmp15339C0E193CBE17FB6CB89DA971471D" />\r
+            <ComponentRef Id="cmp7A634DCF2DA1EDFF9FBAF699758570C3" />\r
+            <ComponentRef Id="cmpAA64F717D23C1375E79BE28E5A3DE005" />\r
+            <ComponentRef Id="cmp14F754092A68F3FE755963981761F031" />\r
+            <ComponentRef Id="cmp8DBD1F81C17E5D01D819E619D30E83CA" />\r
+            <ComponentRef Id="cmp9A97912EFCCFD956EF8F155A81CB494F" />\r
+            <ComponentRef Id="cmp37BA96169B5087F3D2C6E769FF5E9270" />\r
+            <ComponentRef Id="cmpE74780CCB3558FB4C37DDE65264DC1F4" />\r
+            <ComponentRef Id="cmp86D5D0E9BB2DE2BDCC6CF459CB7C4E01" />\r
+            <ComponentRef Id="cmp3951161EB256315C232D7C2B211A5D42" />\r
+            <ComponentRef Id="cmp6CAF0B6CD8ABE85E62A160036392A2CC" />\r
+            <ComponentRef Id="cmp55E217C43921E96FF82040712469E0F3" />\r
+            <ComponentRef Id="cmp57277D9EB23963010750499ED2F541B8" />\r
+            <ComponentRef Id="cmp651F7330940689374F3F4500C1F20484" />\r
+            <ComponentRef Id="cmp02C5C41CDED7F94E885A4A2A28EC3600" />\r
+            <ComponentRef Id="cmp95C32716B4F28A90B070E08CAC81BAC4" />\r
+            <ComponentRef Id="cmp2CEB59611FF3F33F7AEF7D2EAAE0CB05" />\r
+            <ComponentRef Id="cmp8D1031BFABA660F80B7793BB3761EB75" />\r
+            <ComponentRef Id="cmpB52CD2CEE9160F5F67372170543F85B4" />\r
+            <ComponentRef Id="cmp909217E4E53799CCE36ED3F7B817DF15" />\r
+            <ComponentRef Id="cmp88C4BDBD3D48F447FF34FD6DF1FC3C41" />\r
+            <ComponentRef Id="cmp6D134750A96C36CCE37CDAEAC146A810" />\r
+            <ComponentRef Id="cmp17431F800533AAD492A8DA91C537AEE5" />\r
+            <ComponentRef Id="cmp5651D28BAA4D7517AB64D608440B8947" />\r
+            <ComponentRef Id="cmp1FC8B77EF51F392FB0F6CD269098431D" />\r
+            <ComponentRef Id="cmp403E7038C7324A61D8727DC1D939E547" />\r
+            <ComponentRef Id="cmp98D46B3DC15B713ACF5F53B34EE13068" />\r
+            <ComponentRef Id="cmp82998B9C0403FF9F31583386A05AA9BB" />\r
+            <ComponentRef Id="cmp0D0B1F4459593A8B3D4A1BCBA139D98F" />\r
+            <ComponentRef Id="cmpF083A0EDD7D82B7EEFA116A49700462C" />\r
+            <ComponentRef Id="cmp094723DF4DF9079CABB07B397F53FC14" />\r
+            <ComponentRef Id="cmpE46F1304BE1F25953FA1502338B46B6E" />\r
+            <ComponentRef Id="cmpF34990BAAB80CEB1B1FBA3F75A8BB18E" />\r
+            <ComponentRef Id="cmpF8164C6D715D603507F60EA7662104BB" />\r
+            <ComponentRef Id="cmpBC49C3C8C0D1C882B8E4BFA5DDB84097" />\r
+            <ComponentRef Id="cmp026A749908FF7AE58055B0E1AB393168" />\r
+            <ComponentRef Id="cmp4644F2A5A8ADFEA7ED73D09A41CB9AFF" />\r
+            <ComponentRef Id="cmpF0E7DBBAED60C3A0D5E4980C6384C524" />\r
+            <ComponentRef Id="cmpF26383485D9640683A7276A7CFD06280" />\r
+            <ComponentRef Id="cmp5D85AEBC11476CF382025D6654F36103" />\r
+            <ComponentRef Id="cmp44002125F963B5A1D193E0D787FF6AF6" />\r
+            <ComponentRef Id="cmpF8E1020B8AEA4B8F647DF3962BB9CF2C" />\r
+            <ComponentRef Id="cmp6F5B3D19FBA609A3ECE0700E2DB1C805" />\r
+            <ComponentRef Id="cmpF0883486B5B318E646462C05EB5DBF23" />\r
+            <ComponentRef Id="cmp8F7277DF6FE75EF54431CCAB073199B5" />\r
+            <ComponentRef Id="cmpBC3A73EE4B192F1BF88AF2E61E4212EA" />\r
+            <ComponentRef Id="cmpD48A473B45218C0BDE5C64912EFCBAFC" />\r
+            <ComponentRef Id="cmp86201E518FC2273E24BCD76E296C18E1" />\r
+            <ComponentRef Id="cmpFF8739D6C4BEC98E8F1240FFBD0A64B0" />\r
+            <ComponentRef Id="cmp38720475F2109CBC7EF4F43C365CC5CA" />\r
+            <ComponentRef Id="cmp996D8C56417E10EF44329F7339E474F2" />\r
+            <ComponentRef Id="cmpED22A8F3ACEE04AB6CE44D533F4F300A" />\r
+            <ComponentRef Id="cmp838B8853704ACB2A7F45BA00B950F3BB" />\r
+            <ComponentRef Id="cmpDF9CC0BB490A823FFADEFFBDAB92C2C7" />\r
+            <ComponentRef Id="cmp73DF7086A467801478C7321A8B435AA2" />\r
+            <ComponentRef Id="cmp10D7AC86A56DE2D04356715F35250435" />\r
+            <ComponentRef Id="cmpB8BBD8CCBCDE8771AD9E3D6D456FDD6B" />\r
+            <ComponentRef Id="cmp5DA5D6BC1A869E7409CC08ACDEBAA08F" />\r
+            <ComponentRef Id="cmpEFE26691A6CBD74691FA18F89319520B" />\r
+            <ComponentRef Id="cmpF14CA7FF23A8FEBB250D9C1A4DBAB5F2" />\r
+            <ComponentRef Id="cmp76B901918023AFA1ECF5AFE1C5AADFD6" />\r
+            <ComponentRef Id="cmp3B04385170209415EA0392D6C6D9394D" />\r
+            <ComponentRef Id="cmpAC7FCF48DC2DD2FF598D8D8EDFBDDE10" />\r
+            <ComponentRef Id="cmp31295E08BF0ADC9F7414B879A59EAB38" />\r
+            <ComponentRef Id="cmp26ED7398D2B7FD7CE79B20C6F81AA21B" />\r
+            <ComponentRef Id="cmp0AF138C48EF9389FB53BCFFF127C63B7" />\r
+            <ComponentRef Id="cmpB84DF029169B6913288463C0CD791FD7" />\r
+            <ComponentRef Id="cmpB2768FB92A037D37B0EDB0A50917DB6E" />\r
+            <ComponentRef Id="cmp60DE9C3DB4C8AE187B4D19BDEDC6C16A" />\r
+            <ComponentRef Id="cmp8439239F2F45A8EAE3D84F080943DDD1" />\r
+            <ComponentRef Id="cmp16223EE59A9E78D62203D037E29D698D" />\r
+            <ComponentRef Id="cmp018AFFA2C882E3A63FE78C48058C9701" />\r
+            <ComponentRef Id="cmpD9344D05ADD481EACDED9FFB5EE9D160" />\r
+            <ComponentRef Id="cmpA9175F4256E542F0F7D7729B65AC8716" />\r
+            <ComponentRef Id="cmp022C885B9AF26895C6BBDBE1AF964DA7" />\r
+            <ComponentRef Id="cmpB42CC102A79539E49D9D71AF819BDCFA" />\r
+            <ComponentRef Id="cmpDE27E4C6B7C8141F7B15529523211BDE" />\r
+            <ComponentRef Id="cmp8A1FE21942F63A12FE8C262291D2F118" />\r
+            <ComponentRef Id="cmp89930EDDF65A84FB647750A3889D3998" />\r
+            <ComponentRef Id="cmp7A7C588B21E8E3817983B40E6C15A8DC" />\r
+            <ComponentRef Id="cmpFAF5864C58442FAA1D5C0A01C83FACE9" />\r
+            <ComponentRef Id="cmp48641EDC547835A4C015C138C9EE12D6" />\r
+            <ComponentRef Id="cmp74D44A9BA04153CCD0A6FA413CA57C4B" />\r
+            <ComponentRef Id="cmp84CD7E65AAF5D3AD9F421445901CF37D" />\r
+            <ComponentRef Id="cmpAB0EEB7FDAB89DB33CC1872F2D020B9A" />\r
+            <ComponentRef Id="cmpEA5C0DF0E7CC7FF0C0A116E8ED0DFD39" />\r
+            <ComponentRef Id="cmp48F6BA7469D6C5E8A90EFDC2F1F12A03" />\r
+            <ComponentRef Id="cmpEACB1D29768FC4AD7754B7A8BB5CEAA4" />\r
+            <ComponentRef Id="cmp40DFBE5C4004C66BD421833A0916A402" />\r
+            <ComponentRef Id="cmpD46A2953ED55E92D9E0462BE4D66B828" />\r
+            <ComponentRef Id="cmp9BBFEBDFBD798ED64CF1BFB852E57719" />\r
+            <ComponentRef Id="cmpD60B739F43AAC84324A66CEFFCDDBDFF" />\r
+            <ComponentRef Id="cmp4AF8FAB5F76A012B1E00956784FB86C0" />\r
+            <ComponentRef Id="cmpD3F2F08057724AFA25E4EBF3514406A6" />\r
+            <ComponentRef Id="cmp5D978D2112EB628D93B78BBB779859CA" />\r
+            <ComponentRef Id="cmp6800D8FB1FBB983412E92B80451A1109" />\r
+            <ComponentRef Id="cmp71287354FF8DCC602BB0D3D414E05962" />\r
+            <ComponentRef Id="cmp0DF5FCC7255520F9D24CAC298FA859D0" />\r
+            <ComponentRef Id="cmp3301D802CB29561A15AE015C15DD8543" />\r
+            <ComponentRef Id="cmp00A4060669FB650FE92DDD9A09A857C0" />\r
+            <ComponentRef Id="cmp2D3FB42E796C744949D0480EE5C04892" />\r
+            <ComponentRef Id="cmp11B5D7ADB851C95DCFE5C71959222497" />\r
+            <ComponentRef Id="cmp27BC211BA8D2261D578FC390503266AF" />\r
+            <ComponentRef Id="cmp60AB973400004A31DBB4967567BB3780" />\r
+            <ComponentRef Id="cmp026D50205EC5D03323420AC1EC10FD89" />\r
+            <ComponentRef Id="cmpE0BD5867C82F9D8554FECDDC4B6901C7" />\r
+            <ComponentRef Id="cmp9F4E4D4D0534820ED7B1592321BACF51" />\r
+            <ComponentRef Id="cmpCB85EA75A98576ACBC265DECF4F7697F" />\r
+            <ComponentRef Id="cmpD8B043EAF161B1EED3C0D0E0E8B0717E" />\r
+            <ComponentRef Id="cmp006E05B629020152BA06266DCF586F94" />\r
+            <ComponentRef Id="cmp6390600916A9D80DEECC6B64C1A5734B" />\r
+            <ComponentRef Id="cmpA029FDE3C079115EA43947112C138B8C" />\r
+            <ComponentRef Id="cmp050C776C9A28886790788280CCEC3D91" />\r
+            <ComponentRef Id="cmpBFBC5C595627F87399C1078F9A522E9D" />\r
+            <ComponentRef Id="cmp2B0856221A67BDF81C2238000B184A17" />\r
+            <ComponentRef Id="cmp835605203F95C84205B718EDDDE2091E" />\r
+            <ComponentRef Id="cmpD37BF99FBDB8EFD807188027A80B8DA3" />\r
+            <ComponentRef Id="cmp31DA9547A00549AAE408F3DADB011332" />\r
+            <ComponentRef Id="cmpC869C8092B8F2AAC631C286328891A94" />\r
+            <ComponentRef Id="cmp0305CAFC1FCE93ED480CC829AAD832AF" />\r
+            <ComponentRef Id="cmp5D66D11EA79AD27A1AE641BF9C7CB9F2" />\r
+            <ComponentRef Id="cmpA817DF6EA38B1DF29BCE614C5450544B" />\r
+            <ComponentRef Id="cmpFB26590AF40355E4D807DA7009B7836D" />\r
+            <ComponentRef Id="cmpFA168D7F4EFC52948B17DF862E07E25A" />\r
+            <ComponentRef Id="cmp80B129BEE9AED15B717DBBC4E1C91B8D" />\r
+            <ComponentRef Id="cmp222BD8179F2D4ECA9CD649598F1665FE" />\r
+            <ComponentRef Id="cmpE9263BDE73548B76D4CF00FA421C6394" />\r
+            <ComponentRef Id="cmp665380529EE1C0ED1DE23E8B26850B19" />\r
+            <ComponentRef Id="cmp8DD36362549E19ECEA88E3D1B1ACBB85" />\r
+            <ComponentRef Id="cmpD2EA9700B16C8836D5206D1185C549A0" />\r
+            <ComponentRef Id="cmp73D2F27C7DDDD4CF7ADAEBF8E6E42EB1" />\r
+            <ComponentRef Id="cmp7E31792D0EB10DD8AFD37842DFFCA924" />\r
+            <ComponentRef Id="cmpA158D23F2833A86832CC8AB04B51E8D0" />\r
+            <ComponentRef Id="cmpA62BD30E70FBA79DED1CC038B0404EBC" />\r
+            <ComponentRef Id="cmp78F57F7853BF2F83CA4C27E09AE1E09D" />\r
+            <ComponentRef Id="cmpE3E3B7C94E5AFDAF94C8CC78998771DF" />\r
+            <ComponentRef Id="cmpBC000241C708266C86D302A435E66F1C" />\r
+            <ComponentRef Id="cmp3B8600643B709AD9AD113079EDB50C94" />\r
+            <ComponentRef Id="cmpB755C164EDFD73E1498268DB3DA880E5" />\r
+            <ComponentRef Id="cmp1CBDAA564E02886155300F7A8AF049CF" />\r
+            <ComponentRef Id="cmpFAE3866F17FF7039CA9618739C64A1A5" />\r
+            <ComponentRef Id="cmp1BEDE14E6357E29206EDC740AE50650E" />\r
+            <ComponentRef Id="cmp9848706507698E3D2DE99B0C079EAA41" />\r
+            <ComponentRef Id="cmpAF75487C0AF2238D5F5AB27EE11B8C29" />\r
+            <ComponentRef Id="cmpF1D853F627859C5B31EEDA150FC58C6E" />\r
+            <ComponentRef Id="cmp24C324DDA2757C24A5D64657F10D1861" />\r
+            <ComponentRef Id="cmp24A094BA67CBF70909850C9D82D581FB" />\r
+            <ComponentRef Id="cmp855A7D3DC1E9E7D07C156736FB529CB6" />\r
+            <ComponentRef Id="cmpF44D1A83846E86F2D4EFFEDC9DA14661" />\r
+            <ComponentRef Id="cmp15902266961B205BB7C49E3F21B0A166" />\r
+            <ComponentRef Id="cmp50966487B32C316D078827563E31DB30" />\r
+            <ComponentRef Id="cmpA1B9598A2B3B9C203B5780C91F7075D7" />\r
+            <ComponentRef Id="cmpEF587C49FDE4B67961EDEA007128D3DE" />\r
+            <ComponentRef Id="cmp793DBF048FBD4343297F1E17756BBF6B" />\r
+            <ComponentRef Id="cmpD98F61FC599E1BD66057249B28016A0E" />\r
+            <ComponentRef Id="cmp4C6EA3AFEB0324ED3875A46DCCB4EA48" />\r
+            <ComponentRef Id="cmpA7F194C9D57330A5F72C74CD96507E48" />\r
+            <ComponentRef Id="cmp337BDFFE231941F8A3E54BF4AC644B58" />\r
+            <ComponentRef Id="cmp0255A607D5334E94D517A2FEDDB2105D" />\r
+            <ComponentRef Id="cmpBDEB6272B93E7E2FD1982012B5EF3D02" />\r
+            <ComponentRef Id="cmp6ACA30972AFD6D78448054BA4368AEE7" />\r
+            <ComponentRef Id="cmp0014F4AC9A48147F8EAB3F17B2C144E0" />\r
+            <ComponentRef Id="cmpB1ABCFD82FD1858E7657C34A8104404C" />\r
+            <ComponentRef Id="cmp6A432A70B05CC4AC38DA38F776B97812" />\r
+            <ComponentRef Id="cmp134038F5FD44A08B4DA0CFD8921DFB42" />\r
+            <ComponentRef Id="cmp34FB0E37566676B69EEEB98F670D23C4" />\r
+            <ComponentRef Id="cmp166A7C72CA2D82533EBF24FB87FE4BC5" />\r
+            <ComponentRef Id="cmp11D7406D35C2641BC20018F1FA8ADC45" />\r
+            <ComponentRef Id="cmp6CA80FCA9CDD4C55F2582BF139AA4B1B" />\r
+            <ComponentRef Id="cmpDFBFE88B708CE2CA5904473FE0CDB861" />\r
+            <ComponentRef Id="cmp8D47A9ABEDE81A100C924FF154224483" />\r
+            <ComponentRef Id="cmp0071C7F9D6D4B5DC3218D487DABFC218" />\r
+            <ComponentRef Id="cmp54B17CC907A926962454D9FDEA99E369" />\r
+            <ComponentRef Id="cmpDF321BF03AC6D81897FFD4B64045DE88" />\r
+            <ComponentRef Id="cmpEBBBCD3B40ADAF40F5F7BBDAD1628BAB" />\r
+            <ComponentRef Id="cmp4781FFA691E6B55670D19CCA10E68D4A" />\r
+            <ComponentRef Id="cmp9D75099F72BC1EA408C69BAE1036713A" />\r
+            <ComponentRef Id="cmpD507403E3878D92F029FB454CD349781" />\r
+            <ComponentRef Id="cmpDB50F59C1041F3C78FB11F782D92A59C" />\r
+            <ComponentRef Id="cmp9ACED442699FD1B84B592FD9A678FF1F" />\r
+            <ComponentRef Id="cmpC959D8B9C731BB4CB168C53232754164" />\r
+            <ComponentRef Id="cmpF036082D9E70D219B49993BF2078FC9B" />\r
+            <ComponentRef Id="cmp610C62A18FB0CBD4A23E6420089991A6" />\r
+            <ComponentRef Id="cmpF0A89D556198AEC41AA931DE764420C9" />\r
+            <ComponentRef Id="cmp1FB4C766D7DFBDC29927513C52FF444C" />\r
+            <ComponentRef Id="cmpAB3EF735C77E4B6501B74C3070F17E8D" />\r
+            <ComponentRef Id="cmpEF370FB1E0BE2F7C44ACA77A0B9A380F" />\r
+            <ComponentRef Id="cmpD53F72AC172BCE3F947CB966768B983E" />\r
+            <ComponentRef Id="cmpB58DE0208168537FC63A28BA40094D3E" />\r
+            <ComponentRef Id="cmp1E24968394165967A4C933056CC97E8B" />\r
+            <ComponentRef Id="cmpA780D5122E140F762FC4C3A0D02E7179" />\r
+            <ComponentRef Id="cmpE61937584021054E6A0A34AFBB281937" />\r
+            <ComponentRef Id="cmpB2D199D19AF00559ED6198A864F4874E" />\r
+            <ComponentRef Id="cmpA29A335EDA4A585067146163B51D3643" />\r
+            <ComponentRef Id="cmp4F8C12B67EDC519F78AA4840C85494D1" />\r
+            <ComponentRef Id="cmp30F05444D83E8DAFE75CAAB16F50E3AD" />\r
+            <ComponentRef Id="cmp7573B19F4A56CF2B1D359A87D25A39B4" />\r
+            <ComponentRef Id="cmp0B698F624CC72E780FB1FCCBE6340AAD" />\r
+            <ComponentRef Id="cmp898F4E8E360375D6D8FE6F606F899441" />\r
+            <ComponentRef Id="cmp246EDCD16E1C5BC88F69EFDF059C62B6" />\r
+            <ComponentRef Id="cmpB92657C823E717DB4819343891A10BB1" />\r
+            <ComponentRef Id="cmpF4DE6E8F14E6A8E27857D8BC89787BAF" />\r
+            <ComponentRef Id="cmpF49A6AC910C2EB6FF0917041E1F96E8A" />\r
+            <ComponentRef Id="cmp1DF54EA158668A0E21013A820D2E7F51" />\r
+            <ComponentRef Id="cmpDFA6653A3AA375D59BE8318C927D5DC4" />\r
+            <ComponentRef Id="cmp20B23CB93B2BC9BF69A97D0994698310" />\r
+            <ComponentRef Id="cmp735A0D9DB2D96E80F3352A232FCAB063" />\r
+            <ComponentRef Id="cmpDB346892873A00B4024D345FE4C725BA" />\r
+            <ComponentRef Id="cmp32E0784BA82190D0BD64E488EF265F55" />\r
+            <ComponentRef Id="cmp4215773DF3A614E82EF59C456A6EFA97" />\r
+            <ComponentRef Id="cmp72E1D5A67C90BF0538F8015E486898B0" />\r
+            <ComponentRef Id="cmp02A0C8EDA0860CBAE68AE51FAE93EBB1" />\r
+            <ComponentRef Id="cmpC54A96A34D6E8EEB099E5754699E907D" />\r
+            <ComponentRef Id="cmp2564368EA9BDCD6CBD86BD0B47845CE8" />\r
+            <ComponentRef Id="cmp4C037A4C1F7E6103342D757CE84FB3B0" />\r
+            <ComponentRef Id="cmp2782B54139300C706E3DF5D779A32A4E" />\r
+            <ComponentRef Id="cmp5ACAEC60F8EC208DC179E4790FE9090B" />\r
+            <ComponentRef Id="cmpE672C34FD6F4C63A61786B77A67D0BCA" />\r
+            <ComponentRef Id="cmp05B02931195D1A41BF37EA6B3C26F10B" />\r
+            <ComponentRef Id="cmp2214585F3508B759DB1308CC180757FB" />\r
+            <ComponentRef Id="cmpA1474840D69304F1AD34009D58C2B5DA" />\r
+            <ComponentRef Id="cmp40B743148ECA12B22598A157C6147B6E" />\r
+            <ComponentRef Id="cmp104D8EDE2CF475C5EA4FE8169E74E299" />\r
+            <ComponentRef Id="cmpDC8A03C1B37F2CFE043AED01502A741C" />\r
+            <ComponentRef Id="cmp87D7B0E5B5E51059D964EDCED844F4AD" />\r
+            <ComponentRef Id="cmp80E63D0ACF59FD23F5B051FE5BD003DF" />\r
+            <ComponentRef Id="cmp65C97B5A564AB844DB2F02FFA5B163A4" />\r
+            <ComponentRef Id="cmp4A028D7062688CA7458F90092B47AAAC" />\r
 <?endif?>\r
         </ComponentGroup>\r
     </Fragment>\r
index f34dab9d479a91dcc73ce9c47ee71bc46b20c531..d6901b82f74bfe24728fba3861410c9a0d7f4a06 100644 (file)
             <ComponentRef Id='ProgramMenuDir' />\r
         </Feature>\r
 \r
+        <Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch JackTrip" />\r
+        <Property Id="WixShellExecTarget" Value="[INSTALLDIR]\jacktrip.exe" />\r
+        <CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />\r
+\r
         <WixVariable Id='WixUIDialogBmp' Value='dialog.bmp' />\r
         <WixVariable Id='WixUILicenseRtf' Value='license.rtf' />\r
         <Property Id='WIXUI_INSTALLDIR' Value='INSTALLDIR' />\r
-        <UIRef Id='WixUI_InstallDir' />\r
+        <UI>\r
+            <UIRef Id='WixUI_InstallDir' />\r
+            <Publish Dialog="ExitDialog"\r
+                Control="Finish" \r
+                Event="DoAction" \r
+                Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>\r
+        </UI>\r
         <Icon Id='jacktrip.exe' SourceFile='jacktrip.exe' />\r
     </Product>\r
 </Wix>\r