mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-10-30 07:53:33 +08:00
Compare commits
44 Commits
nightly/20
...
beta/20250
| Author | SHA1 | Date | |
|---|---|---|---|
| 524c9b0ed4 | |||
| 8677349c4a | |||
| 905b2e858e | |||
| 6fc2e75a33 | |||
| c738caa3a4 | |||
| cb13d4f771 | |||
| 436e11a6a4 | |||
| 869aef8929 | |||
| e0f939318e | |||
| 136dea011f | |||
| af09269d81 | |||
| 5a5ef4711d | |||
| 1dd745d446 | |||
| 3dd2d26d86 | |||
| eb6b6a2b93 | |||
| b15d692a0e | |||
| e76bcbd555 | |||
| b5a979e6b1 | |||
| 898444dd3c | |||
| 99f246016d | |||
| e24f3d91e8 | |||
| 3a7850b398 | |||
| 1e1750024b | |||
| 2ba53a2e40 | |||
| ceec1f95b9 | |||
| 1ac3db4f33 | |||
| 93d3d18c7b | |||
| d4b7891f48 | |||
| d7c294edd0 | |||
| 945cfe176d | |||
| fa3a153896 | |||
| e9106b2bcc | |||
| a967518d45 | |||
| 7ebed53e97 | |||
| ca05963c40 | |||
| df98c6c3fd | |||
| 05501e33e9 | |||
| 91780ae400 | |||
| 4900ca9f1b | |||
| 0f965aae28 | |||
| f8c29fc4a1 | |||
| 2e1889caf1 | |||
| 8070d1bfc2 | |||
| 4ee1a309a1 |
2
3rdparty/hunspell
vendored
2
3rdparty/hunspell
vendored
Submodule 3rdparty/hunspell updated: 525f9f2276...749cd84a0b
@ -84,10 +84,8 @@ if(WIN32)
|
||||
if(BETA)
|
||||
message(STATUS "Beta config enabled")
|
||||
add_definitions(-DBETA)
|
||||
set(JAMI_OUTPUT_DIRECTORY_RELEASE ${PROJECT_SOURCE_DIR}/x64/Beta)
|
||||
else()
|
||||
set(JAMI_OUTPUT_DIRECTORY_RELEASE ${PROJECT_SOURCE_DIR}/x64/Release)
|
||||
endif()
|
||||
set(JAMI_OUTPUT_DIRECTORY_RELEASE ${PROJECT_SOURCE_DIR}/x64/${CMAKE_BUILD_TYPE})
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
@ -266,6 +264,7 @@ add_custom_target(
|
||||
-DAPP_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}
|
||||
-DCORE_SOURCE_DIR=${DAEMON_DIR}
|
||||
-DCPP_INT_FILE=${VERSION_INFO_DIR}/version_info.cpp.in
|
||||
-DBUILD_VERSION=${BUILD_VERSION}
|
||||
-P ${CMAKE_SCRIPTS_DIR}/generate_version_info.cmake
|
||||
)
|
||||
list(APPEND CLIENT_INCLUDE_DIRS ${VERSION_INFO_DIR})
|
||||
@ -347,7 +346,8 @@ set(COMMON_SOURCES
|
||||
${APP_SRC_DIR}/conversationlistmodel.cpp
|
||||
${APP_SRC_DIR}/searchresultslistmodel.cpp
|
||||
${APP_SRC_DIR}/calloverlaymodel.cpp
|
||||
${APP_SRC_DIR}/spellcheckdictionarymanager.cpp
|
||||
${APP_SRC_DIR}/spellcheckdictionarylistmodel.cpp
|
||||
${APP_SRC_DIR}/spellcheckadapter.cpp
|
||||
${APP_SRC_DIR}/filestosendlistmodel.cpp
|
||||
${APP_SRC_DIR}/wizardviewstepmodel.cpp
|
||||
${APP_SRC_DIR}/avatarregistry.cpp
|
||||
@ -378,7 +378,6 @@ set(COMMON_HEADERS
|
||||
${APP_SRC_DIR}/appversionmanager.h
|
||||
${APP_SRC_DIR}/utils.h
|
||||
${APP_SRC_DIR}/bannedlistmodel.h
|
||||
${APP_SRC_DIR}/version.h
|
||||
${APP_SRC_DIR}/accountlistmodel.h
|
||||
${APP_SRC_DIR}/instancemanager.h
|
||||
${APP_SRC_DIR}/connectivitymonitor.h
|
||||
@ -420,7 +419,8 @@ set(COMMON_HEADERS
|
||||
${APP_SRC_DIR}/conversationlistmodel.h
|
||||
${APP_SRC_DIR}/searchresultslistmodel.h
|
||||
${APP_SRC_DIR}/calloverlaymodel.h
|
||||
${APP_SRC_DIR}/spellcheckdictionarymanager.h
|
||||
${APP_SRC_DIR}/spellcheckdictionarylistmodel.h
|
||||
${APP_SRC_DIR}/spellcheckadapter.h
|
||||
${APP_SRC_DIR}/filestosendlistmodel.h
|
||||
${APP_SRC_DIR}/wizardviewstepmodel.h
|
||||
${APP_SRC_DIR}/avatarregistry.h
|
||||
@ -474,33 +474,10 @@ endif()
|
||||
find_package(PkgConfig REQUIRED)
|
||||
|
||||
# hunspell
|
||||
pkg_check_modules(HUNSPELL hunspell)
|
||||
if(MSVC)
|
||||
elseif (NOT APPLE)
|
||||
set(HUNSPELL_DICT_DIR "/usr/share/hunspell/")
|
||||
else()
|
||||
set(HUNSPELL_DICT_DIR "/Library/Spelling/")
|
||||
endif()
|
||||
|
||||
if(HUNSPELL_FOUND)
|
||||
pkg_search_module(hunspell IMPORTED_TARGET hunspell)
|
||||
if(hunspell_FOUND)
|
||||
message(STATUS "hunspell found")
|
||||
include_directories(${HUNSPELL_INCLUDE_DIR})
|
||||
find_path(HUNSPELL_INCLUDE_DIRS
|
||||
NAMES hunspell.hxx
|
||||
PATH_SUFFIXES hunspell
|
||||
HINTS ${HUNSPELL_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
find_library(HUNSPELL_LIBRARIES
|
||||
NAMES ${HUNSPELL_LIBRARIES}
|
||||
hunspell
|
||||
hunspell-1.7
|
||||
libhunspell
|
||||
libhunspell-1.7
|
||||
libhunspell-devel
|
||||
libhunspell-dev
|
||||
HINTS ${HUNSPELL_LIBRARY_DIRS}
|
||||
)
|
||||
set(HUNSPELL_LIBRARIES PkgConfig::hunspell)
|
||||
else()
|
||||
message(STATUS "hunspell not found - building hunspell")
|
||||
|
||||
@ -717,20 +694,34 @@ add_subdirectory(3rdparty/SortFilterProxyModel)
|
||||
set(SFPM_OBJECTS $<TARGET_OBJECTS:SortFilterProxyModel>)
|
||||
|
||||
# md4c
|
||||
set(BUILD_MD2HTML_EXECUTABLE OFF CACHE BOOL "Don't build md2html executable" FORCE)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Don't build shared md4c library" FORCE)
|
||||
add_subdirectory(3rdparty/md4c EXCLUDE_FROM_ALL)
|
||||
list(APPEND CLIENT_LINK_DIRS ${MD4C_BINARY_DIR}/src)
|
||||
list(APPEND CLIENT_INCLUDE_DIRS ${MD4C_SOURCE_DIR}/src)
|
||||
list(APPEND CLIENT_LIBS md4c-html)
|
||||
find_package(md4c)
|
||||
if(md4c_FOUND)
|
||||
message(STATUS "Using system-provided md4c-html")
|
||||
list(APPEND CLIENT_LIBS md4c::md4c-html)
|
||||
else()
|
||||
message("Using bundled md4c-html library")
|
||||
set(BUILD_MD2HTML_EXECUTABLE OFF CACHE BOOL "Don't build md2html executable" FORCE)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Don't build shared md4c library" FORCE)
|
||||
add_subdirectory(3rdparty/md4c EXCLUDE_FROM_ALL)
|
||||
list(APPEND CLIENT_LINK_DIRS ${MD4C_BINARY_DIR}/src)
|
||||
list(APPEND CLIENT_INCLUDE_DIRS ${MD4C_SOURCE_DIR}/src)
|
||||
list(APPEND CLIENT_LIBS md4c-html)
|
||||
endif()
|
||||
|
||||
# tidy-html5
|
||||
set(BUILD_SHARED_LIB OFF CACHE BOOL "Don't build shared tidy library" FORCE)
|
||||
set(SUPPORT_CONSOLE_APP OFF CACHE BOOL "Don't build tidy console app" FORCE)
|
||||
add_subdirectory(3rdparty/tidy-html5 EXCLUDE_FROM_ALL)
|
||||
list(APPEND CLIENT_LINK_DIRS ${tidy_BINARY_DIR}/Release)
|
||||
list(APPEND CLIENT_INCLUDE_DIRS ${tidy_SOURCE_DIR}/include)
|
||||
list(APPEND CLIENT_LIBS tidy-static)
|
||||
pkg_check_modules(tidy IMPORTED_TARGET tidy)
|
||||
if(tidy_FOUND)
|
||||
message(STATUS "Using system-provided tidy")
|
||||
list(APPEND CLIENT_LIBS PkgConfig::tidy)
|
||||
else()
|
||||
message("Using bundled tidy library")
|
||||
set(BUILD_SHARED_LIB OFF CACHE BOOL "Don't build shared tidy library" FORCE)
|
||||
set(SUPPORT_CONSOLE_APP OFF CACHE BOOL "Don't build tidy console app" FORCE)
|
||||
add_subdirectory(3rdparty/tidy-html5 EXCLUDE_FROM_ALL)
|
||||
list(APPEND CLIENT_LINK_DIRS ${tidy_BINARY_DIR}/Release)
|
||||
list(APPEND CLIENT_INCLUDE_DIRS ${tidy_SOURCE_DIR}/include)
|
||||
list(APPEND CLIENT_LIBS tidy-static)
|
||||
endif()
|
||||
|
||||
# ZXing-cpp configuration
|
||||
set(BUILD_EXAMPLES OFF CACHE BOOL "")
|
||||
@ -750,9 +741,10 @@ qt_add_executable(
|
||||
${COMMON_SOURCES}
|
||||
${QML_RESOURCES}
|
||||
${QML_RESOURCES_QML}
|
||||
${SFPM_OBJECTS})
|
||||
${SFPM_OBJECTS}
|
||||
src/app/spellcheckadapter.h src/app/spellcheckadapter.cpp)
|
||||
|
||||
add_dependencies(${PROJECT_NAME} hunspell)
|
||||
#add_dependencies(${PROJECT_NAME} hunspell)
|
||||
|
||||
# Ensure the generated version file can be found.
|
||||
add_dependencies(${PROJECT_NAME} generate_version_info)
|
||||
|
||||
@ -4,10 +4,5 @@
|
||||
<?define ExeName="Jami" ?>
|
||||
<?define AppName="Jami" ?>
|
||||
<?define Manufacturer="Savoir-Faire Linux"?>
|
||||
|
||||
<?if $(var.Configuration) = Release ?>
|
||||
<?define ReleaseDir="..\x64\Release"?>
|
||||
<?else?>
|
||||
<?define ReleaseDir="..\x64\Beta"?>
|
||||
<?endif ?>
|
||||
</Include>
|
||||
|
||||
@ -16,9 +16,9 @@
|
||||
<InstallerPlatform>x64</InstallerPlatform>
|
||||
<DefineSolutionProperties>false</DefineSolutionProperties>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath>
|
||||
<PropertyGroup>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<IntermediateOutputPath>obj\Release\</IntermediateOutputPath>
|
||||
<DefineConstants>AppHarvestPath=..\x64\Release;CrtHarvestPath=$(VC_CRT_Dir)</DefineConstants>
|
||||
<SuppressPdbOutput>True</SuppressPdbOutput>
|
||||
<CompilerAdditionalOptions>
|
||||
@ -26,16 +26,6 @@
|
||||
<WixVariables>
|
||||
</WixVariables>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Beta|x64' ">
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath>
|
||||
<DefineConstants>AppHarvestPath=..\x64\Beta;CrtHarvestPath=$(VC_CRT_Dir)</DefineConstants>
|
||||
<SuppressPdbOutput>True</SuppressPdbOutput>
|
||||
<CompilerAdditionalOptions>
|
||||
</CompilerAdditionalOptions>
|
||||
<WixVariables>
|
||||
</WixVariables>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Product.wxs" />
|
||||
<Compile Include="AppComponents.wxs" />
|
||||
@ -70,7 +60,7 @@
|
||||
<Error Text="The WiX Toolset v3.11 (or newer) build tools must be installed to build this project. To download the WiX Toolset, see http://wixtoolset.org/releases/" />
|
||||
</Target>
|
||||
<Target Name="BeforeBuild">
|
||||
<HeatDirectory Directory="..\x64\$(Configuration)"
|
||||
<HeatDirectory Directory="..\x64\Release"
|
||||
PreprocessorVariable="var.AppHarvestPath"
|
||||
OutputFile="AppComponents.wxs"
|
||||
ComponentGroupName="AppHeatGenerated"
|
||||
|
||||
2
daemon
2
daemon
Submodule daemon updated: c231df3e05...1969975c2c
@ -24,6 +24,7 @@ set(CMAKE_CURRENT_BINARY_DIR ${APP_BINARY_DIR})
|
||||
# Generate the version string for the application and core
|
||||
configure_version_string(${APP_SOURCE_DIR} APP_VERSION_STRING)
|
||||
configure_version_string(${CORE_SOURCE_DIR} CORE_VERSION_STRING)
|
||||
set(BUILD_VERSION_STRING ${BUILD_VERSION})
|
||||
|
||||
# Get output file names with the .in extension removed
|
||||
get_filename_component(VERSION_CPP_FILENAME ${CPP_INT_FILE} NAME_WE)
|
||||
|
||||
@ -4,11 +4,24 @@ ENV DEBIAN_FRONTEND noninteractive
|
||||
ENV QT_QUICK_BACKEND software
|
||||
ENV QT_QPA_PLATFORM offscreen
|
||||
|
||||
RUN apt-get clean
|
||||
RUN apt-get update && \
|
||||
apt-get install -y devscripts equivs
|
||||
apt-get install -y --no-install-recommends ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Use only the custom Savoir-faire Linux Ubuntu mirror
|
||||
RUN rm -f /etc/apt/sources.list /etc/apt/sources.list.d/* && \
|
||||
echo "deb http://gpl.savoirfairelinux.net/pub/mirrors/ubuntu jammy main restricted universe multiverse" > /etc/apt/sources.list && \
|
||||
echo "deb http://gpl.savoirfairelinux.net/pub/mirrors/ubuntu jammy-updates main restricted universe multiverse" >> /etc/apt/sources.list && \
|
||||
echo "deb http://gpl.savoirfairelinux.net/pub/mirrors/ubuntu jammy-security main restricted universe multiverse" >> /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
devscripts \
|
||||
equivs \
|
||||
gnupg \
|
||||
dirmngr \
|
||||
curl
|
||||
|
||||
RUN apt install gnupg dirmngr ca-certificates curl --no-install-recommends
|
||||
RUN curl -s https://dl.jami.net/public-key.gpg | tee /usr/share/keyrings/jami-archive-keyring.gpg > /dev/null
|
||||
RUN sh -c "echo 'deb [signed-by=/usr/share/keyrings/jami-archive-keyring.gpg] https://dl.jami.net/internal/ubuntu_22.04/ jami main' > /etc/apt/sources.list.d/jami.list"
|
||||
RUN apt-get update && apt-get install libqt-jami -y
|
||||
@ -69,7 +82,8 @@ RUN apt-get install -y pandoc \
|
||||
libcppunit-dev \
|
||||
googletest \
|
||||
libgtest-dev \
|
||||
wget
|
||||
wget && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install a recent version of CMake
|
||||
ADD extras/packaging/gnu-linux/scripts/install-cmake.sh /opt/install-cmake.sh
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
[Desktop Entry]
|
||||
Name=Jami
|
||||
GenericName=Jami
|
||||
Comment=Privacy-oriented voice, video, chat, and conference platform
|
||||
Comment[hu]=Adatvédelem-orientált hang-, video-, csevegés- és konferenciaplatform
|
||||
Comment[ru]=Jami — приложение для защищённой связи с распределённой архитектурой
|
||||
Comment=Share, freely and privately
|
||||
Comment[hu]=Megosztás, szabadon és bizalmasan
|
||||
Exec=jami %u
|
||||
Icon=jami
|
||||
StartupNotify=true
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;Telephony;
|
||||
Categories=Chat;FileTransfer;InstantMessaging;Network;P2P;Telephony;VideoConference;
|
||||
Keywords=Qt;chat;talk;im;message;voip;
|
||||
MimeType=x-scheme-handler/jami;
|
||||
MimeType=x-scheme-handler/jami;
|
||||
|
||||
@ -2,87 +2,91 @@
|
||||
<!-- Copyright (C) 2015-2025 Savoir-faire Linux Inc. -->
|
||||
<component type="desktop-application">
|
||||
<id>net.jami.Jami</id>
|
||||
<metadata_license>CC-BY-SA-3.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<name>Jami</name>
|
||||
|
||||
<summary>Privacy-oriented voice, video, chat, and conference platform</summary>
|
||||
<summary xml:lang="hu">Adatvédelem-orientált hang-, video-, csevegés- és konferenciaplatform</summary>
|
||||
<summary>Share, freely and privately</summary>
|
||||
<summary xml:lang="hu">Megosztás, szabadon és bizalmasan</summary>
|
||||
|
||||
<icon type="stock">jami</icon>
|
||||
|
||||
<metadata_license>CC-BY-SA-3.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
|
||||
<developer id="net.jami.Jami">
|
||||
<name>Savoir-faire Linux Inc.</name>
|
||||
</developer>
|
||||
|
||||
<!-- https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-description -->
|
||||
<description>
|
||||
<p>Jami, a GNU package, is software for universal and distributed peer-to-peer communication that respects the freedom and privacy of its users.</p>
|
||||
<p xml:lang="hu">A Jami, egy GNU-csomag, egy univerzális és elosztott társ-társ kommunikációra szolgáló szoftver, amely tiszteletben tartja a felhasználók szabadságát és magánéletét.</p>
|
||||
<p>Jami is the simplest and easiest way to connect with people (and devices) with instant messaging, audio and video calls over the Internet and LAN/WAN intranets.</p>
|
||||
<p xml:lang="hu">A Jami a legegyszerűbb és legegyszerűbb módja annak, hogy azonnali üzenetküldéssel, hang- és videohívásokkal kapcsolódjon az emberekhez (és eszközökhöz) az interneten és a LAN/WAN intraneteken keresztül.</p>
|
||||
|
||||
<p><em>Jami</em>, a GNU package, is software for universal and distributed peer-to-peer communication that respects the freedom and privacy of its users.</p>
|
||||
<p xml:lang="hu">A <em>Jami</em>, egy GNU-csomag, egy univerzális és elosztott társ-társ kommunikációt szolgáló szoftver, amely tiszteletben tartja a felhasználók szabadságát és magánéletét.</p>
|
||||
|
||||
<p>Jami is the simplest and easiest way to connect with people (and devices) with instant messaging, audio, and video calls over the Internet and LAN/WAN intranets.</p>
|
||||
<p xml:lang="hu">A Jami a legegyszerűbb és legegyszerűbb módja annak, hogy azonnali üzenetküldéssel, hang- és videohívásokkal kapcsolódjon az emberekhez (és eszközökhöz) az interneten és a LAN/WAN intraneten keresztül.</p>
|
||||
|
||||
<p>Jami is a free/libre, end-to-end encrypted, and private communication platform.</p>
|
||||
<p xml:lang="hu">A Jami egy ingyenes, teljes körűen titkosított és privát kommunikációs platform.</p>
|
||||
<p>Jami – which used to be known as Ring – is also an open-source alternative (to Facebook Messenger, Signal, Skype, Teams, Telegram, TikTok, Viber, WhatsApp, Zoom) that prioritizes the privacy of its users.</p>
|
||||
<p xml:lang="hu">A Jami – amelyet korábban Ring néven ismertek – egy nyílt forráskódú alternatíva is (a Facebook Messenger, a Signal, a Skype, a Teams, a Telegram, a TikTok, a Viber, a WhatsApp, a Zoom számára), amely előtérbe helyezi a felhasználók magánéletét.</p>
|
||||
<p>Jami has a professional-looking design and is available for a wide range of platforms. Unlike the alternatives, calls using Jami are directly between users as it does not use servers to handle calls.</p>
|
||||
<p xml:lang="hu">A Jami professzionális megjelenésű, és platformok széles skálájához elérhető. Az alternatívákkal ellentétben a Jami-t használó hívások közvetlenül a felhasználók között zajlanak, mivel nem használ kiszolgálókat a hívások kezelésére.</p>
|
||||
<p>This gives the greatest privacy as the distributed nature of Jami means your calls are only between participants.</p>
|
||||
|
||||
<p>Jami is open-source software that prioritizes user privacy.</p>
|
||||
<p xml:lang="hu">A Jami nyílt forráskódú szoftver, amely előnyben részesíti a felhasználók adatvédelmét.</p>
|
||||
|
||||
<p>Jami has a professional-looking design and is available for a wide range of platforms. Unlike the alternatives, Jami calls are directly between users, as it does not use servers to handle calls.</p>
|
||||
<p xml:lang="hu">A Jami professzionális megjelenésű, és platformok széles skálájához elérhető. Az alternatívákkal ellentétben a Jami közvetlenül a felhasználók között zajlik, mivel nem használ kiszolgálókat a hívások kezelésére.</p>
|
||||
|
||||
<p>This gives the greatest privacy, as the distributed nature of Jami means your calls are only between participants.</p>
|
||||
<p xml:lang="hu">Ez biztosítja a legnagyobb magánéletet, mivel a Jami elosztott jellege azt jelenti, hogy a hívások csak a résztvevők között zajlanak.</p>
|
||||
<p>One-to-one and group conversations with Jami are enhanced with: instant messaging; audio and video calling; recording and sending audio and video messages; file transfers; screen sharing; and, location sharing.</p>
|
||||
<p xml:lang="hu">A Jamival folytatott személyes és csoportos beszélgetéseket a következők javítják: azonnali üzenetküldés; hang- és videohívások; hang- és videoüzenetek rögzítése és küldése; fájlátvitel; képernyőmegosztás; és helymegosztás.</p>
|
||||
|
||||
<p>One-to-one and group conversations with Jami are enhanced with instant messaging, audio and video calling, recording and sending audio and video messages, file transfers, screen sharing, and location sharing.</p>
|
||||
<p xml:lang="hu">A Jamival folytatott személyes és csoportos beszélgetéseket az azonnali üzenetküldés, hang- és videohívások, hang- és videoüzenetek rögzítése és küldése, fájlátvitel, képernyőmegosztás és helymegosztás továbbfejlesztik.</p>
|
||||
|
||||
<p>Jami can also function as a SIP client.</p>
|
||||
<p xml:lang="hu">A Jami SIP-ügyfélként is működhet.</p>
|
||||
<p>Jami has multiple extensions available: Audio Filter; Auto Answer; Green Screen; Watermark; and, Whisper Transcript.</p>
|
||||
<p xml:lang="hu">A Jami-nek több bővítménye is elérhető: hangszűrő; automatikus válasz; zöld képernyő; vízjel; és, suttogó átirat.</p>
|
||||
<p>Jami can be easily deployed in organizations with the “Jami Account Management Server” (JAMS), allowing users to connect with their corporate credentials or create local accounts. JAMS allows you to manage your own Jami community while taking advantage of Jami’s distributed network architecture.</p>
|
||||
<p xml:lang="hu">A Jami könnyen telepíthető a szervezetekben a JAMS (Jami Account Management Server – Jami fiókkezelő kiszolgáló), amely lehetővé teszi a felhasználók számára, hogy csatlakozzanak vállalati hitelesítő adataikhoz, vagy helyi fiókokat hozzanak létre. A JAMS lehetővé teszi saját Jami közösségének kezelését, miközben kihasználja a Jami elosztott hálózati architektúráját.</p>
|
||||
<p>Jami is available for GNU/Linux, Windows, macOS, iOS, Android, and Android TV, making Jami an interoperable and cross-platform communication framework.</p>
|
||||
<p xml:lang="hu">A Jami elérhető GNU/Linux, Windows, macOS, iOS, Android és Android TV rendszereken, így a Jami egy interoperábilis és platformok közötti kommunikációs keretrendszer.</p>
|
||||
<p>Manage multiple SIP accounts, Jami accounts and JAMS accounts with the Jami client installed on one or multiple devices.</p>
|
||||
<p xml:lang="hu">Kezeljen több SIP-fiókot, Jami-fiókot és JAMS-fiókot az egy vagy több eszközre telepített Jami-ügyféllel.</p>
|
||||
<p>Jami is free, unlimited, private, advertising free, compatible, fast, autonomous, and anonymous.</p>
|
||||
|
||||
<p>Multiple Jami extensions are available: Audio Filter, Auto Answer, Green Screen, Segmentation, Watermark, and Whisper Transcript.</p>
|
||||
<p xml:lang="hu">Több Jami bővítmény érhető el: hangszűrő, automatikus válasz, zöld képernyő, szegmentálás, vízjel és suttogó átirat.</p>
|
||||
|
||||
<p>Jami can be easily deployed in organizations with the <em>JAMS (Jami Account Management Server)</em>, allowing users to connect with their corporate credentials or create local accounts. JAMS allows you to manage your own Jami community while taking advantage of Jami’s distributed network architecture.</p>
|
||||
<p xml:lang="hu">A Jami könnyen telepíthető a szervezetekben a <em>JAMS (Jami Account Management Server — JAMS-kezelő-kiszolgáló)</em> segítségével, amely lehetővé teszi a felhasználók számára, hogy csatlakozzanak vállalati hitelesítő adataikhoz, vagy helyi fiókokat hozzanak létre. A JAMS lehetővé teszi a saját Jami közösség kezelését, miközben kihasználja a Jami elosztott hálózati architektúráját.</p>
|
||||
|
||||
<p>Jami is available for GNU/Linux, Windows, macOS, iOS, Android, Android TV, and web browsers, making Jami an interoperable and cross-platform communication framework.</p>
|
||||
<p xml:lang="hu">A Jami elérhető GNU/Linux, Windows, macOS, iOS, Android, Android TV és webböngészők számára, így a Jami interoperábilis és platformok közötti kommunikációs keretrendszerré válik.</p>
|
||||
|
||||
<p>Manage multiple SIP accounts, Jami accounts, and JAMS accounts with the Jami client installed on one or multiple devices.</p>
|
||||
<p xml:lang="hu">Több SIP-, Jami- és JAMS-fiók kezelése az egy vagy több eszközre telepített a Jami-alkalmazással.</p>
|
||||
|
||||
<p>Jami is free, unlimited, private, advertising‑free, compatible, fast, autonomous, and anonymous.</p>
|
||||
<p xml:lang="hu">A Jami ingyenes, korlátlan, privát, reklámmentes, kompatibilis, gyors, autonóm és névtelen.</p>
|
||||
<p>Learn more about:</p>
|
||||
<p xml:lang="hu">További tájékoztatás:</p>
|
||||
<ul>
|
||||
<li>Jami: https://jami.net/</li>
|
||||
<li xml:lang="hu">Jami: https://jami.net/hu/</li>
|
||||
<li>Jami extensions: https://jami.net/extensions/</li>
|
||||
<li xml:lang="hu">Jami-bővítmények: https://jami.net/hu/extensions/</li>
|
||||
<li>“Jami Account Management Server” (JAMS): https://jami.biz/</li>
|
||||
<li xml:lang="hu">JAMS (Jami Account Management Server – Jami fiókkezelő kiszolgáló): https://jami.biz/</li>
|
||||
<li>Jami documentation: https://docs.jami.net/</li>
|
||||
<li xml:lang="hu">Jami-dokumentáció: https://docs.jami.net/hu/</li>
|
||||
</ul>
|
||||
<p>Follow us for more:</p>
|
||||
<p xml:lang="hu">Kövess minket a továbbiakért:</p>
|
||||
<ul>
|
||||
<li>Mastodon: https://mstdn.io/@Jami</li>
|
||||
<li xml:lang="hu">Mastodon: https://mstdn.io/@Jami</li>
|
||||
<li>X: https://x.com/jami_social</li>
|
||||
<li xml:lang="hu">X: https://x.com/jami_social</li>
|
||||
<li>YouTube: https://www.youtube.com/@jami9311</li>
|
||||
<li xml:lang="hu">YouTube: https://www.youtube.com/@jami9311</li>
|
||||
</ul>
|
||||
<p>We’d love to hear from you! Join the Jami community:</p>
|
||||
<p xml:lang="hu">Szívesen hallanánk felőled! Csatlakozzon a Jami közösséghez:</p>
|
||||
<ul>
|
||||
<li>Contribute: https://jami.net/contribute/</li>
|
||||
<li xml:lang="hu">Közreműködés: https://jami.net/hu/contribute/</li>
|
||||
<li>Forum: https://forum.jami.net/</li>
|
||||
<li xml:lang="hu">Fórum: https://forum.jami.net/</li>
|
||||
</ul>
|
||||
<p>Build with Jami on your IoT project: re-use the universal communications technology of Jami with its portable library on your system of choice.</p>
|
||||
<p xml:lang="hu">Építsen a Jamival IoT-projektjére: használja újra a Jami univerzális kommunikációs technológiáját a hordozható könyvtárával a választott rendszerén.</p>
|
||||
|
||||
<p>Build IoT projects with Jami. Re-use the universal communications technology of Jami with its portable library on your system of choice.</p>
|
||||
<p xml:lang="hu">Építsen IoT-projekteket Jamival. Használja újra a Jami univerzális kommunikációs technológiáját annak hordozható könyvtárával az Ön által választott rendszeren.</p>
|
||||
|
||||
<p>Jami for Android TV is tested on NVIDIA SHIELD TV with Logitech cameras.</p>
|
||||
<p xml:lang="hu">A Jami for Android TV-t Logitech kamerákkal ellátott NVIDIA SHIELD TV-n tesztelték.</p>
|
||||
<p>Jami is published under the GPL license, version 3 or higher.</p>
|
||||
<p xml:lang="hu">A Jami a GPL licenc 3-as vagy újabb verziója alatt jelent meg.</p>
|
||||
<p>Copyright © Savoir-faire Linux Inc.</p>
|
||||
<p xml:lang="hu">Szerzői jog © Savoir-faire Linux Inc.</p>
|
||||
<p xml:lang="hu">A Jami Androidra TV-t Logitech kamerákkal ellátott NVIDIA SHIELD TV-n tesztelték.</p>
|
||||
|
||||
<p>Jami is published under the GPL license, version 3 or higher. Copyright © Savoir-faire Linux Inc.</p>
|
||||
<p xml:lang="hu">A Jami a GPL-licenc 3-as vagy újabb verziója alatt jelent meg. Szerzői jog © Savoir-faire Linux Inc.</p>
|
||||
|
||||
</description>
|
||||
|
||||
<url type="homepage">https://jami.net/</url>
|
||||
<requires>
|
||||
<id>net.jami.daemon</id>
|
||||
</requires>
|
||||
|
||||
<url type="bugtracker">https://git.jami.net/savoirfairelinux/jami-client-qt/issues</url>
|
||||
<url type="faq">https://docs.jami.net/user/faq.html</url>
|
||||
<url type="help">https://forum.jami.net/</url>
|
||||
<url type="homepage">https://jami.net/</url>
|
||||
<url type="donation">https://jami.net/whydonate/</url>
|
||||
<url type="translate">https://www.transifex.com/savoirfairelinux/jami</url>
|
||||
<url type="contact">contact@jami.net</url>
|
||||
<url type="faq">https://docs.jami.net/user/faq.html</url>
|
||||
<url type="translate">https://explore.transifex.com/savoirfairelinux/jami/</url>
|
||||
<url type="contribute">https://jami.net/contribute/</url>
|
||||
<url type="help">https://docs.jami.net/</url>
|
||||
<url type="vcs-browser">https://git.jami.net/savoirfairelinux</url>
|
||||
|
||||
<provides>
|
||||
<binary>jami</binary>
|
||||
<mediatype>x-scheme-handler/jami</mediatype>
|
||||
</provides>
|
||||
|
||||
<!-- Maximum caption length is 60 characters -->
|
||||
<!-- Officially GIF is not an allowed video format, but it appears to work nonetheless -->
|
||||
@ -121,7 +125,15 @@
|
||||
|
||||
<launchable type="desktop-id">net.jami.Jami.desktop</launchable>
|
||||
|
||||
<provides><binary>jami</binary></provides>
|
||||
<!-- https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-keywords -->
|
||||
<keywords>
|
||||
<keyword translate="no">Qt</keyword>
|
||||
<keyword>chat</keyword>
|
||||
<keyword>talk</keyword>
|
||||
<keyword>im</keyword>
|
||||
<keyword>message</keyword>
|
||||
<keyword translate="no">voip</keyword>
|
||||
</keywords>
|
||||
|
||||
<!-- https://specifications.freedesktop.org/menu-spec/latest/apa.html -->
|
||||
<!-- https://specifications.freedesktop.org/menu-spec/latest/apas02.html -->
|
||||
@ -142,5 +154,4 @@
|
||||
<content_attribute id="social-audio">intense</content_attribute>
|
||||
</content_rating>
|
||||
|
||||
<requires><id>net.jami.daemon</id></requires>
|
||||
</component>
|
||||
|
||||
4
extras/packaging/gnu-linux/Jenkinsfile
vendored
4
extras/packaging/gnu-linux/Jenkinsfile
vendored
@ -85,6 +85,10 @@ pipeline {
|
||||
|
||||
environment {
|
||||
TARBALLS = '/var/cache/jami' // set the cache directory
|
||||
BUILD_VERSION = sh(
|
||||
returnStdout: true,
|
||||
script: 'date +"%Y%m%d%H%M"'
|
||||
).trim()
|
||||
}
|
||||
|
||||
stages {
|
||||
|
||||
@ -169,12 +169,9 @@ DISTRIBUTIONS := \
|
||||
ubuntu_24.04 \
|
||||
ubuntu_24.10 \
|
||||
ubuntu_25.04 \
|
||||
fedora_39 \
|
||||
fedora_40 \
|
||||
fedora_41 \
|
||||
fedora_42 \
|
||||
alma_9 \
|
||||
opensuse-leap_15.5 \
|
||||
opensuse-leap_15.6 \
|
||||
snap
|
||||
|
||||
@ -194,6 +191,7 @@ $(1)-docker-image-name := jami-packaging-$(1)
|
||||
$(1)-docker-image-file := .docker-image-$$($(1)-docker-image-name)
|
||||
$(1)-docker-run-command := docker run \
|
||||
--rm --privileged --security-opt apparmor=docker-default \
|
||||
-e BUILD_VERSION=${BUILD_VERSION} \
|
||||
-e RELEASE_VERSION="$(RELEASE_VERSION)" \
|
||||
-e RELEASE_DIRNAME="$(RELEASE_DIRNAME)" \
|
||||
-e RELEASE_TARBALL_FILENAME="$(RELEASE_TARBALL_FILENAME)" \
|
||||
|
||||
@ -70,9 +70,8 @@ RUN dnf install -y \
|
||||
libdrm \
|
||||
gperf \
|
||||
bison \
|
||||
clang \
|
||||
clang-devel \
|
||||
llvm-devel \
|
||||
clang16-devel \
|
||||
llvm16-devel \
|
||||
nodejs \
|
||||
flex \
|
||||
gstreamer1 gstreamer1-devel \
|
||||
@ -96,7 +95,6 @@ RUN dnf install -y \
|
||||
perl-English \
|
||||
libxshmfence-devel \
|
||||
ninja-build \
|
||||
clang \
|
||||
cmake \
|
||||
fmt-devel \
|
||||
python3-html5lib \
|
||||
|
||||
@ -11,14 +11,6 @@ RUN apt-get update && \
|
||||
libdbus-1-dev \
|
||||
wget
|
||||
|
||||
# As of January 2024, the default compiler on Debian unstable is GCC 13.2.0, which
|
||||
# is unable to build one of Qt 6.6.1's dependencies, see:
|
||||
# https://github.com/qt/qtquick3d-assimp/commit/253f8bfa621a9fa6cd2c36291cdaa8c60c99322c
|
||||
# The linked commit above fixes the problem and is included in more recent versions of Qt.
|
||||
# For now, we use GCC 12 as a temporary workaround:
|
||||
ADD extras/packaging/gnu-linux/scripts/install-gcc-debian.sh /opt/install-gcc-debian.sh
|
||||
RUN /opt/install-gcc-debian.sh 12
|
||||
|
||||
ADD extras/packaging/gnu-linux/scripts/prebuild-package-debian.sh /opt/prebuild-package-debian.sh
|
||||
|
||||
COPY extras/packaging/gnu-linux/rules/debian-qt/control /tmp/builddeps/debian/control
|
||||
|
||||
@ -1,105 +0,0 @@
|
||||
FROM fedora:39
|
||||
|
||||
RUN dnf clean all
|
||||
RUN dnf update -y
|
||||
|
||||
RUN dnf install -y dnf-command\(builddep\) rpmdevtools && \
|
||||
dnf install -y mock
|
||||
|
||||
RUN dnf groupinstall -y "X Software Development"
|
||||
|
||||
RUN dnf install -y \
|
||||
git \
|
||||
rpm-build \
|
||||
tar \
|
||||
make \
|
||||
autoconf \
|
||||
automake \
|
||||
nasm \
|
||||
speexdsp-devel \
|
||||
pulseaudio-libs-devel \
|
||||
libcanberra-devel \
|
||||
libcurl-devel \
|
||||
libtool \
|
||||
mesa-libgbm-devel \
|
||||
mesa-dri-drivers \
|
||||
dbus-devel \
|
||||
expat-devel \
|
||||
pcre-devel \
|
||||
yaml-cpp-devel \
|
||||
libXext-devel \
|
||||
libXfixes-devel \
|
||||
yasm \
|
||||
speex-devel \
|
||||
gsm-devel \
|
||||
chrpath \
|
||||
check \
|
||||
astyle \
|
||||
uuid-c++-devel \
|
||||
gettext-devel \
|
||||
gcc-c++ \
|
||||
which \
|
||||
alsa-lib-devel \
|
||||
systemd-devel \
|
||||
libuuid-devel \
|
||||
uuid-devel \
|
||||
gnutls-devel \
|
||||
nettle-devel \
|
||||
opus-devel \
|
||||
patch \
|
||||
jsoncpp-devel \
|
||||
libnatpmp-devel \
|
||||
webkitgtk4-devel \
|
||||
cryptopp-devel \
|
||||
libva-devel \
|
||||
libvdpau-devel \
|
||||
msgpack-devel \
|
||||
NetworkManager-libnm-devel \
|
||||
openssl-devel \
|
||||
clutter-devel \
|
||||
clutter-gtk-devel \
|
||||
libappindicator-gtk3-devel \
|
||||
libnotify-devel \
|
||||
libupnp-devel \
|
||||
qrencode-devel \
|
||||
libargon2-devel \
|
||||
libsndfile-devel \
|
||||
libdrm \
|
||||
gperf \
|
||||
bison \
|
||||
clang \
|
||||
clang-devel \
|
||||
llvm-devel \
|
||||
nodejs \
|
||||
flex \
|
||||
gstreamer1 gstreamer1-devel \
|
||||
gstreamer1-plugins-base-devel \
|
||||
gstreamer1-plugins-good \
|
||||
gstreamer1-plugins-bad-free-devel \
|
||||
nss-devel \
|
||||
libxcb* \
|
||||
libxkb* \
|
||||
libX11-devel \
|
||||
vulkan-devel \
|
||||
libXrender-devel \
|
||||
xcb-util-* \
|
||||
xz \
|
||||
xkeyboard-config \
|
||||
libnotify \
|
||||
wget \
|
||||
libstdc++-static \
|
||||
sqlite-devel \
|
||||
perl-generators \
|
||||
perl-English \
|
||||
libxshmfence-devel \
|
||||
ninja-build \
|
||||
clang \
|
||||
cmake \
|
||||
fmt-devel \
|
||||
python3.10 \
|
||||
cups-devel \
|
||||
pipewire-devel
|
||||
|
||||
ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-rpm.sh
|
||||
|
||||
CMD ["/opt/build-package-rpm.sh"]
|
||||
@ -1,105 +0,0 @@
|
||||
FROM fedora:40
|
||||
|
||||
RUN dnf clean all
|
||||
RUN dnf update -y
|
||||
|
||||
RUN dnf install -y dnf-command\(builddep\) rpmdevtools && \
|
||||
dnf install -y mock
|
||||
|
||||
RUN dnf groupinstall -y "X Software Development"
|
||||
|
||||
RUN dnf install -y \
|
||||
git \
|
||||
rpm-build \
|
||||
tar \
|
||||
make \
|
||||
autoconf \
|
||||
automake \
|
||||
nasm \
|
||||
speexdsp-devel \
|
||||
pulseaudio-libs-devel \
|
||||
libcanberra-devel \
|
||||
libcurl-devel \
|
||||
libtool \
|
||||
mesa-libgbm-devel \
|
||||
mesa-dri-drivers \
|
||||
dbus-devel \
|
||||
expat-devel \
|
||||
pcre-devel \
|
||||
yaml-cpp-devel \
|
||||
libXext-devel \
|
||||
libXfixes-devel \
|
||||
yasm \
|
||||
speex-devel \
|
||||
gsm-devel \
|
||||
chrpath \
|
||||
check \
|
||||
astyle \
|
||||
uuid-c++-devel \
|
||||
gettext-devel \
|
||||
gcc-c++ \
|
||||
which \
|
||||
alsa-lib-devel \
|
||||
systemd-devel \
|
||||
libuuid-devel \
|
||||
uuid-devel \
|
||||
gnutls-devel \
|
||||
nettle-devel \
|
||||
opus-devel \
|
||||
patch \
|
||||
jsoncpp-devel \
|
||||
libnatpmp-devel \
|
||||
webkitgtk4-devel \
|
||||
cryptopp-devel \
|
||||
libva-devel \
|
||||
libvdpau-devel \
|
||||
msgpack-devel \
|
||||
NetworkManager-libnm-devel \
|
||||
openssl-devel \
|
||||
clutter-devel \
|
||||
clutter-gtk-devel \
|
||||
libappindicator-gtk3-devel \
|
||||
libnotify-devel \
|
||||
libupnp-devel \
|
||||
qrencode-devel \
|
||||
libargon2-devel \
|
||||
libsndfile-devel \
|
||||
libdrm \
|
||||
gperf \
|
||||
bison \
|
||||
clang \
|
||||
clang-devel \
|
||||
llvm-devel \
|
||||
nodejs \
|
||||
flex \
|
||||
gstreamer1 gstreamer1-devel \
|
||||
gstreamer1-plugins-base-devel \
|
||||
gstreamer1-plugins-good \
|
||||
gstreamer1-plugins-bad-free-devel \
|
||||
nss-devel \
|
||||
libxcb* \
|
||||
libxkb* \
|
||||
libX11-devel \
|
||||
vulkan-devel \
|
||||
libXrender-devel \
|
||||
xcb-util-* \
|
||||
xz \
|
||||
xkeyboard-config \
|
||||
libnotify \
|
||||
wget \
|
||||
libstdc++-static \
|
||||
sqlite-devel \
|
||||
perl-generators \
|
||||
perl-English \
|
||||
libxshmfence-devel \
|
||||
ninja-build \
|
||||
clang \
|
||||
cmake \
|
||||
fmt-devel \
|
||||
python3.10 \
|
||||
cups-devel \
|
||||
pipewire-devel
|
||||
|
||||
ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-rpm.sh
|
||||
|
||||
CMD ["/opt/build-package-rpm.sh"]
|
||||
@ -1,117 +0,0 @@
|
||||
FROM opensuse/leap:15.5
|
||||
|
||||
RUN zypper --gpg-auto-import-keys refresh
|
||||
|
||||
RUN zypper --non-interactive install -y \
|
||||
dnf \
|
||||
dnf-command\(builddep\) \
|
||||
rpmdevtools \
|
||||
Mesa-dri-devel Mesa-dri \
|
||||
git \
|
||||
gcc10 \
|
||||
gcc10-c++ \
|
||||
rpm-build \
|
||||
tar \
|
||||
make \
|
||||
autoconf \
|
||||
automake \
|
||||
nasm \
|
||||
speexdsp-devel \
|
||||
libpulse-devel \
|
||||
libcanberra-devel \
|
||||
libcurl-devel \
|
||||
libtool \
|
||||
pcre-devel \
|
||||
yaml-cpp-devel \
|
||||
libXext-devel \
|
||||
libXfixes-devel \
|
||||
yasm \
|
||||
speex-devel \
|
||||
libgsm-devel \
|
||||
chrpath \
|
||||
check \
|
||||
astyle \
|
||||
gettext-devel \
|
||||
which \
|
||||
alsa-lib-devel \
|
||||
systemd-devel \
|
||||
libuuid-devel \
|
||||
uuid-devel \
|
||||
libopus-devel \
|
||||
patch \
|
||||
jsoncpp-devel \
|
||||
webkit2gtk3-devel \
|
||||
libcryptopp-devel \
|
||||
libva-devel \
|
||||
libvdpau-devel \
|
||||
msgpack-c-devel \
|
||||
msgpack-cxx-devel \
|
||||
clutter-devel \
|
||||
openssl-devel \
|
||||
clutter-gtk-devel \
|
||||
libnma-devel \
|
||||
libcryptopp-devel \
|
||||
libexpat-devel \
|
||||
gnome-icon-theme-symbolic \
|
||||
libgsm-devel \
|
||||
gtk3-devel \
|
||||
libappindicator-devel \
|
||||
sqlite-devel \
|
||||
ffmpeg-4-libavutil-devel \
|
||||
gtk3-devel\
|
||||
qrencode-devel \
|
||||
python310 \
|
||||
python3-python-dateutil \
|
||||
python3-html5lib \
|
||||
libsndfile-devel \
|
||||
libdrm \
|
||||
gperf \
|
||||
bison \
|
||||
flex \
|
||||
ffmpeg ffmpeg-devel \
|
||||
nodejs20 \
|
||||
mozilla-nss-devel \
|
||||
python-xml \
|
||||
python3-six \
|
||||
python3-importlib-metadata \
|
||||
libxcb* \
|
||||
libxkb* \
|
||||
libX11-devel \
|
||||
libXrender-devel \
|
||||
libfreetype6 \
|
||||
xcb-util-image-devel \
|
||||
xcb-util-keysyms-devel \
|
||||
xcb-util-renderutil-devel \
|
||||
xcb-util-wm-devel \
|
||||
xorg-x11-devel \
|
||||
xz \
|
||||
xkeyboard-config \
|
||||
libnotify \
|
||||
argon2-devel \
|
||||
libxshmfence-devel \
|
||||
xproto-devel \
|
||||
xcb-proto-devel \
|
||||
xcb-* \
|
||||
xorg-* \
|
||||
vulkan-devel \
|
||||
ninja \
|
||||
gstreamer-devel \
|
||||
gstreamer-plugins-good \
|
||||
gstreamer-plugins-bad-devel \
|
||||
gstreamer-plugins-base-devel \
|
||||
cmake \
|
||||
wget \
|
||||
pipewire-devel
|
||||
|
||||
# openSUSE Leap 15.5 comes with Python 3.6 by default,
|
||||
# but we need at least 3.7 to compile Qt 6.6.1
|
||||
RUN rm /usr/bin/python3 && ln -s /usr/bin/python3.10 /usr/bin/python3
|
||||
|
||||
RUN update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 50
|
||||
RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 50
|
||||
|
||||
ADD extras/packaging/gnu-linux/scripts/build-package-rpm.sh /opt/build-package-rpm.sh
|
||||
|
||||
ENV CC=gcc
|
||||
ENV CXX=g++
|
||||
CMD ["/opt/build-package-rpm.sh"]
|
||||
@ -36,12 +36,6 @@ RUN curl -L $(curl -H 'X-Ubuntu-Series: 16' 'https://api.snapcraft.io/api/v1/sna
|
||||
RUN mkdir -p /snap/snapcraft
|
||||
RUN unsquashfs -d /snap/snapcraft/current snapcraft.snap
|
||||
|
||||
# Fix Python3 installation: Make sure we use the interpreter from
|
||||
# the snapcraft snap:
|
||||
RUN unlink /snap/snapcraft/current/usr/bin/python3
|
||||
RUN ln -s /snap/snapcraft/current/usr/bin/python3.* /snap/snapcraft/current/usr/bin/python3
|
||||
RUN echo /snap/snapcraft/current/lib/python3.*/site-packages >> /snap/snapcraft/current/usr/lib/python3/dist-packages/site-packages.pth
|
||||
|
||||
# Create a snapcraft runner
|
||||
RUN mkdir -p /snap/bin
|
||||
RUN echo "#!/bin/sh" > /snap/bin/snapcraft
|
||||
|
||||
@ -261,7 +261,7 @@ Build-Depends: debhelper (>= 9),
|
||||
gperf,
|
||||
khronos-api,
|
||||
# libasound2-dev [linux-any],
|
||||
libavcodec-dev (>= 7:3.4.8~),
|
||||
libavcodec-dev (>= 7:3.4.8~) | libavcodec-extra-dev (>= 7:3.4.8~),
|
||||
libavformat-dev (>= 7:3.4.8~),
|
||||
libavutil-dev (>= 7:3.4.8~),
|
||||
libcap-dev [linux-any],
|
||||
|
||||
@ -30,7 +30,7 @@ Build-Depends: debhelper (>= 9),
|
||||
libspeex-dev,
|
||||
libspeexdsp-dev,
|
||||
uuid-dev,
|
||||
libavcodec-dev,
|
||||
libavcodec-dev | libavcodec-extra-dev,
|
||||
libavutil-dev,
|
||||
libavformat-dev,
|
||||
libswscale-dev,
|
||||
|
||||
@ -74,7 +74,7 @@ override_dh_auto_build:
|
||||
--disable-gsm \
|
||||
--disable-speexdsp \
|
||||
--disable-natpmp \
|
||||
--enable-gnutls $(BUNDLED_PKGS) && \
|
||||
$(BUNDLED_PKGS) && \
|
||||
make list && \
|
||||
make -j$(NO_CPUS) V=1
|
||||
cd daemon && \
|
||||
@ -91,6 +91,7 @@ override_dh_auto_build:
|
||||
mkdir build && \
|
||||
cd build && \
|
||||
cmake \
|
||||
-DBUILD_VERSION=$(BUILD_VERSION) \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DLIBJAMI_BUILD_DIR=$(CURDIR)/daemon/src \
|
||||
-DENABLE_LIBWRAP=true \
|
||||
|
||||
@ -1,16 +1,7 @@
|
||||
%define name jami
|
||||
%define version RELEASE_VERSION
|
||||
%define release 0
|
||||
|
||||
# The AppStream 1.0 spec says that the catalog file must be put in /usr/share/swcatalog/xml
|
||||
# (see https://www.freedesktop.org/software/appstream/docs/chap-CatalogData.html).
|
||||
#
|
||||
# However, openSUSE Leap still uses the legacy path /usr/share/app-info/xmls as of version 15.5.
|
||||
%if 0%{?sle_version} && 0%{?sle_version} <= 150500
|
||||
%define appstream_catalog_dir /share/app-info/xmls
|
||||
%else
|
||||
%define appstream_catalog_dir /share/swcatalog/xml
|
||||
%endif
|
||||
|
||||
# Exclude vendored Qt6 from dependency generator
|
||||
%define __requires_exclude ^libQt6.*$
|
||||
@ -82,6 +73,7 @@ cd %{_builddir}/jami-%{version} && \
|
||||
-DAPPSTREAM_CATALOG_DIR=%{appstream_catalog_dir} \
|
||||
-DWITH_DAEMON_SUBMODULE=true \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_VERSION=${BUILD_VERSION} \
|
||||
..
|
||||
make -C %{_builddir}/jami-%{version}/build %{_smp_mflags} V=2
|
||||
|
||||
|
||||
@ -293,7 +293,8 @@ parts:
|
||||
cmake .. -DENABLE_LIBWRAP=true \
|
||||
-DLIBJAMI_BUILD_DIR=$SNAPCRAFT_PART_BUILD/daemon/src \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_VERSION=BUILD_VERSION_PLACEHOLDER
|
||||
make -j$SNAPCRAFT_PARALLEL_BUILD_COUNT
|
||||
DESTDIR=$SNAPCRAFT_PART_INSTALL make install
|
||||
build-packages:
|
||||
|
||||
@ -101,10 +101,6 @@ if [ ! -f "${RPM_PATH}" ]; then
|
||||
# Cache the built Qt RPM package.
|
||||
if [[ "${DISTRIBUTION:0:4}" == "rhel" ]]; then
|
||||
cp /root/rpmbuild/RPMS/x86_64/jami-libqt-$QT_MAJOR_MINOR_PATCH-*.el8.x86_64.rpm "${RPM_PATH}"
|
||||
elif [[ "${DISTRIBUTION}" == "fedora_39" ]]; then
|
||||
cp /root/rpmbuild/RPMS/x86_64/jami-libqt-$QT_MAJOR_MINOR_PATCH-*.fc39.x86_64.rpm "${RPM_PATH}"
|
||||
elif [[ "${DISTRIBUTION}" == "fedora_40" ]]; then
|
||||
cp /root/rpmbuild/RPMS/x86_64/jami-libqt-$QT_MAJOR_MINOR_PATCH-*.fc40.x86_64.rpm "${RPM_PATH}"
|
||||
elif [[ "${DISTRIBUTION}" == "fedora_41" ]]; then
|
||||
cp /root/rpmbuild/RPMS/x86_64/jami-libqt-$QT_MAJOR_MINOR_PATCH-*.fc41.x86_64.rpm "${RPM_PATH}"
|
||||
elif [[ "${DISTRIBUTION}" == "fedora_42" ]]; then
|
||||
@ -134,7 +130,7 @@ rpmbuild --define "debug_package %{nil}" -ba jami-libclient.spec
|
||||
rpmbuild --define "debug_package %{nil}" -ba jami-qt.spec
|
||||
|
||||
# Build the Qt client.
|
||||
rpmbuild --define "debug_package %{nil}" -ba jami.spec
|
||||
rpmbuild --define "debug_package %{nil}" --define "BUILD_VERSION ${BUILD_VERSION}" -ba jami.spec
|
||||
|
||||
# Move the built packages to the output directory.
|
||||
mv /root/rpmbuild/RPMS/*/* /opt/output
|
||||
|
||||
@ -29,6 +29,9 @@ cp -r extras/packaging/gnu-linux/rules/snap/${SNAP_PKG_NAME}/snapcraft.yaml .
|
||||
# set the version and tarball filename
|
||||
sed -i "s/RELEASE_VERSION/${RELEASE_VERSION}/g" snapcraft.yaml
|
||||
|
||||
# set the build version of the app
|
||||
sed -i "s/BUILD_VERSION_PLACEHOLDER/${BUILD_VERSION}/g" snapcraft.yaml
|
||||
|
||||
snapcraft # requires snapcraft >= 4.8
|
||||
|
||||
# move the built snap to output
|
||||
|
||||
@ -263,7 +263,7 @@ def cmake_build(config_str, env_vars, cmake_build_dir):
|
||||
return True
|
||||
|
||||
|
||||
def build(config_str, qt_dir, tests, enable_crash_reports, crash_report_url=None):
|
||||
def build(config_str, qt_dir, tests, build_version, enable_crash_reports, crash_report_url=None):
|
||||
"""Use cmake to build the project."""
|
||||
print("Building with Qt at " + qt_dir)
|
||||
|
||||
@ -294,6 +294,9 @@ def build(config_str, qt_dir, tests, enable_crash_reports, crash_report_url=None
|
||||
else:
|
||||
cmake_options.append("-DENABLE_CRASHREPORTS=OFF")
|
||||
|
||||
if build_version:
|
||||
cmake_options.append("-DBUILD_VERSION=" + build_version)
|
||||
|
||||
# Make sure the build directory exists.
|
||||
if not os.path.exists(build_dir):
|
||||
os.makedirs(build_dir)
|
||||
@ -307,11 +310,11 @@ def build(config_str, qt_dir, tests, enable_crash_reports, crash_report_url=None
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def deploy_runtimes(config_str, qt_dir):
|
||||
def deploy_runtimes(qt_dir):
|
||||
"""Deploy the dependencies to the runtime directory."""
|
||||
print("Deploying runtime dependencies")
|
||||
|
||||
runtime_dir = os.path.join(repo_root_dir, "x64", config_str)
|
||||
runtime_dir = os.path.join(repo_root_dir, "x64", "Release")
|
||||
stamp_file = os.path.join(runtime_dir, ".deploy.stamp")
|
||||
if os.path.exists(stamp_file):
|
||||
return
|
||||
@ -470,6 +473,8 @@ def parse_args():
|
||||
help='Sets the Qt root path')
|
||||
parser.add_argument(
|
||||
"-a", "--arch", default="x64", help="Sets the build architecture")
|
||||
parser.add_argument(
|
||||
"--build-version", help="Sets the build version string used for defining app build version")
|
||||
parser.add_argument(
|
||||
"-t", "--tests", action="store_true", help="Build and run tests")
|
||||
parser.add_argument(
|
||||
@ -552,10 +557,11 @@ def main():
|
||||
def do_build(do_tests):
|
||||
if not parsed_args.skip_build:
|
||||
build(config_str, parsed_args.qt, do_tests,
|
||||
parsed_args.build_version,
|
||||
parsed_args.enable_crash_reports,
|
||||
parsed_args.crash_report_url)
|
||||
if not parsed_args.skip_deploy:
|
||||
deploy_runtimes(config_str, parsed_args.qt)
|
||||
deploy_runtimes(parsed_args.qt)
|
||||
|
||||
if parsed_args.subcommand == "pack":
|
||||
do_build(False)
|
||||
|
||||
382
resources/misc/available_dictionaries.json
Normal file
382
resources/misc/available_dictionaries.json
Normal file
@ -0,0 +1,382 @@
|
||||
{
|
||||
"af_ZA": {
|
||||
"nativeName": "Afrikaans (Suid-Afrika)",
|
||||
"path": "af_ZA/af_ZA"
|
||||
},
|
||||
"an_ES": {
|
||||
"nativeName": "aragonés (España)",
|
||||
"path": "an_ES/an_ES"
|
||||
},
|
||||
"ar": {
|
||||
"nativeName": "العربية",
|
||||
"path": "ar/ar"
|
||||
},
|
||||
"as_IN": {
|
||||
"nativeName": "অসমীয়া (भारत)",
|
||||
"path": "as_IN/as_IN"
|
||||
},
|
||||
"be-official": {
|
||||
"nativeName": "беларуская",
|
||||
"path": "be_BY/be-official"
|
||||
},
|
||||
"bg_BG": {
|
||||
"nativeName": "български",
|
||||
"path": "bg_BG/bg_BG"
|
||||
},
|
||||
"bn_BD": {
|
||||
"nativeName": "বাঙ্গালি (বাংলাদেশ)",
|
||||
"path": "bn_BD/bn_BD"
|
||||
},
|
||||
"bn_IN": {
|
||||
"nativeName": "বাঙ্গালি (ভারত)",
|
||||
"path": "bn_BD/bn_IN"
|
||||
},
|
||||
"bo": {
|
||||
"nativeName": "བོད་སྐད་",
|
||||
"path": "bo/bo"
|
||||
},
|
||||
"br_FR": {
|
||||
"nativeName": "breton (France)",
|
||||
"path": "br_FR/br_FR"
|
||||
},
|
||||
"bs_BA": {
|
||||
"nativeName": "bosanski",
|
||||
"path": "bs_BA/bs_BA"
|
||||
},
|
||||
"cs_CZ": {
|
||||
"nativeName": "čeština",
|
||||
"path": "cs_CZ/cs_CZ"
|
||||
},
|
||||
"da_DK": {
|
||||
"nativeName": "dansk",
|
||||
"path": "da_DK/da_DK"
|
||||
},
|
||||
"de_AT_frami": {
|
||||
"nativeName": "Deutsch (Österreich)",
|
||||
"path": "de/de_AT_frami"
|
||||
},
|
||||
"de_CH_frami": {
|
||||
"nativeName": "Deutsch (Schweiz)",
|
||||
"path": "de/de_CH_frami"
|
||||
},
|
||||
"de_DE_frami": {
|
||||
"nativeName": "Deutsch (Deutschland)",
|
||||
"path": "de/de_DE_frami"
|
||||
},
|
||||
"el_GR": {
|
||||
"nativeName": "Ελληνικά",
|
||||
"path": "el_GR/el_GR"
|
||||
},
|
||||
"en_AU": {
|
||||
"nativeName": "English (Australia)",
|
||||
"path": "en/en_AU"
|
||||
},
|
||||
"en_CA": {
|
||||
"nativeName": "English (Canada)",
|
||||
"path": "en/en_CA"
|
||||
},
|
||||
"en_GB": {
|
||||
"nativeName": "English (British)",
|
||||
"path": "en/en_GB"
|
||||
},
|
||||
"en_US": {
|
||||
"nativeName": "English (American)",
|
||||
"path": "en/en_US"
|
||||
},
|
||||
"en_ZA": {
|
||||
"nativeName": "English (Suid-Afrika)",
|
||||
"path": "en/en_ZA"
|
||||
},
|
||||
"eo": {
|
||||
"nativeName": "esperanto",
|
||||
"path": "eo/eo"
|
||||
},
|
||||
"es_AR": {
|
||||
"nativeName": "español (Argentina)",
|
||||
"path": "es/es_AR"
|
||||
},
|
||||
"es_BO": {
|
||||
"nativeName": "español (Bolivia)",
|
||||
"path": "es/es_BO"
|
||||
},
|
||||
"es_CL": {
|
||||
"nativeName": "español (Chile)",
|
||||
"path": "es/es_CL"
|
||||
},
|
||||
"es_CO": {
|
||||
"nativeName": "español (Colombia)",
|
||||
"path": "es/es_CO"
|
||||
},
|
||||
"es_CR": {
|
||||
"nativeName": "español (Costa Rica)",
|
||||
"path": "es/es_CR"
|
||||
},
|
||||
"es_CU": {
|
||||
"nativeName": "español (Cuba)",
|
||||
"path": "es/es_CU"
|
||||
},
|
||||
"es_DO": {
|
||||
"nativeName": "español (República Dominicana)",
|
||||
"path": "es/es_DO"
|
||||
},
|
||||
"es_EC": {
|
||||
"nativeName": "español (Ecuador)",
|
||||
"path": "es/es_EC"
|
||||
},
|
||||
"es_ES": {
|
||||
"nativeName": "español (España)",
|
||||
"path": "es/es_ES"
|
||||
},
|
||||
"es_GQ": {
|
||||
"nativeName": "español (Guinea Ecuatorial)",
|
||||
"path": "es/es_GQ"
|
||||
},
|
||||
"es_GT": {
|
||||
"nativeName": "español (Guatemala)",
|
||||
"path": "es/es_GT"
|
||||
},
|
||||
"es_HN": {
|
||||
"nativeName": "español (Honduras)",
|
||||
"path": "es/es_HN"
|
||||
},
|
||||
"es_MX": {
|
||||
"nativeName": "español (México)",
|
||||
"path": "es/es_MX"
|
||||
},
|
||||
"es_NI": {
|
||||
"nativeName": "español (Nicaragua)",
|
||||
"path": "es/es_NI"
|
||||
},
|
||||
"es_PA": {
|
||||
"nativeName": "español (Panamá)",
|
||||
"path": "es/es_PA"
|
||||
},
|
||||
"es_PE": {
|
||||
"nativeName": "español (Perú)",
|
||||
"path": "es/es_PE"
|
||||
},
|
||||
"es_PH": {
|
||||
"nativeName": "español (Pilipinas)",
|
||||
"path": "es/es_PH"
|
||||
},
|
||||
"es_PR": {
|
||||
"nativeName": "español (Puerto Rico)",
|
||||
"path": "es/es_PR"
|
||||
},
|
||||
"es_PY": {
|
||||
"nativeName": "español (Paraguai)",
|
||||
"path": "es/es_PY"
|
||||
},
|
||||
"es_SV": {
|
||||
"nativeName": "español (El Salvador)",
|
||||
"path": "es/es_SV"
|
||||
},
|
||||
"es_US": {
|
||||
"nativeName": "español (United States)",
|
||||
"path": "es/es_US"
|
||||
},
|
||||
"es_UY": {
|
||||
"nativeName": "español (Uruguay)",
|
||||
"path": "es/es_UY"
|
||||
},
|
||||
"es_VE": {
|
||||
"nativeName": "español (Venezuela)",
|
||||
"path": "es/es_VE"
|
||||
},
|
||||
"et_EE": {
|
||||
"nativeName": "eesti",
|
||||
"path": "et_EE/et_EE"
|
||||
},
|
||||
"fa-IR": {
|
||||
"nativeName": "فارسی",
|
||||
"path": "fa_IR/fa-IR"
|
||||
},
|
||||
"fr": {
|
||||
"nativeName": "français",
|
||||
"path": "fr_FR/fr"
|
||||
},
|
||||
"gd_GB": {
|
||||
"nativeName": "Gàidhlig",
|
||||
"path": "gd_GB/gd_GB"
|
||||
},
|
||||
"gl_ES": {
|
||||
"nativeName": "galego",
|
||||
"path": "gl/gl_ES"
|
||||
},
|
||||
"gu_IN": {
|
||||
"nativeName": "ગુજરાતી",
|
||||
"path": "gu_IN/gu_IN"
|
||||
},
|
||||
"he_IL": {
|
||||
"nativeName": "עברית",
|
||||
"path": "he_IL/he_IL"
|
||||
},
|
||||
"hi_IN": {
|
||||
"nativeName": "हिन्दी",
|
||||
"path": "hi_IN/hi_IN"
|
||||
},
|
||||
"hr_HR": {
|
||||
"nativeName": "hrvatski",
|
||||
"path": "hr_HR/hr_HR"
|
||||
},
|
||||
"hu_HU": {
|
||||
"nativeName": "magyar",
|
||||
"path": "hu_HU/hu_HU"
|
||||
},
|
||||
"id_ID": {
|
||||
"nativeName": "Indonesian",
|
||||
"path": "id/id_ID"
|
||||
},
|
||||
"is": {
|
||||
"nativeName": "íslenska",
|
||||
"path": "is/is"
|
||||
},
|
||||
"it_IT": {
|
||||
"nativeName": "italiano",
|
||||
"path": "it_IT/it_IT"
|
||||
},
|
||||
"kmr_Latn": {
|
||||
"nativeName": "Northern Kurdish",
|
||||
"path": "kmr_Latn/kmr_Latn"
|
||||
},
|
||||
"kn_IN": {
|
||||
"nativeName": "ಕನ್ನಡ",
|
||||
"path": "kn_IN/kn_IN"
|
||||
},
|
||||
"ko_KR": {
|
||||
"nativeName": "한국어",
|
||||
"path": "ko_KR/ko_KR"
|
||||
},
|
||||
"lo_LA": {
|
||||
"nativeName": "ລາວ",
|
||||
"path": "lo_LA/lo_LA"
|
||||
},
|
||||
"lt": {
|
||||
"nativeName": "lietuvių",
|
||||
"path": "lt_LT/lt"
|
||||
},
|
||||
"lv_LV": {
|
||||
"nativeName": "latviešu",
|
||||
"path": "lv_LV/lv_LV"
|
||||
},
|
||||
"mn_MN": {
|
||||
"nativeName": "монгол",
|
||||
"path": "mn_MN/mn_MN"
|
||||
},
|
||||
"mr_IN": {
|
||||
"nativeName": "मराठी",
|
||||
"path": "mr_IN/mr_IN"
|
||||
},
|
||||
"nb_NO": {
|
||||
"nativeName": "norsk bokmål",
|
||||
"path": "no/nb_NO"
|
||||
},
|
||||
"ne_NP": {
|
||||
"nativeName": "नेपाली",
|
||||
"path": "ne_NP/ne_NP"
|
||||
},
|
||||
"nl_NL": {
|
||||
"nativeName": "Nederlands",
|
||||
"path": "nl_NL/nl_NL"
|
||||
},
|
||||
"nn_NO": {
|
||||
"nativeName": "norsk nynorsk",
|
||||
"path": "no/nn_NO"
|
||||
},
|
||||
"oc_FR": {
|
||||
"nativeName": "occitan",
|
||||
"path": "oc_FR/oc_FR"
|
||||
},
|
||||
"or_IN": {
|
||||
"nativeName": "ଓଡ଼ିଆ",
|
||||
"path": "or_IN/or_IN"
|
||||
},
|
||||
"pa_IN": {
|
||||
"nativeName": "ਪੰਜਾਬੀ",
|
||||
"path": "pa_IN/pa_IN"
|
||||
},
|
||||
"pl_PL": {
|
||||
"nativeName": "polski",
|
||||
"path": "pl_PL/pl_PL"
|
||||
},
|
||||
"pt_BR": {
|
||||
"nativeName": "português (Brasil)",
|
||||
"path": "pt_BR/pt_BR"
|
||||
},
|
||||
"pt_PT": {
|
||||
"nativeName": "português europeu (Portugal)",
|
||||
"path": "pt_PT/pt_PT"
|
||||
},
|
||||
"ro_RO": {
|
||||
"nativeName": "română",
|
||||
"path": "ro/ro_RO"
|
||||
},
|
||||
"ru_RU": {
|
||||
"nativeName": "русский",
|
||||
"path": "ru_RU/ru_RU"
|
||||
},
|
||||
"sa_IN": {
|
||||
"nativeName": "संस्कृत भाषा",
|
||||
"path": "sa_IN/sa_IN"
|
||||
},
|
||||
"si_LK": {
|
||||
"nativeName": "සිංහල",
|
||||
"path": "si_LK/si_LK"
|
||||
},
|
||||
"sk_SK": {
|
||||
"nativeName": "slovenčina",
|
||||
"path": "sk_SK/sk_SK"
|
||||
},
|
||||
"sl_SI": {
|
||||
"nativeName": "slovenščina",
|
||||
"path": "sl_SI/sl_SI"
|
||||
},
|
||||
"sq_AL": {
|
||||
"nativeName": "shqip",
|
||||
"path": "sq_AL/sq_AL"
|
||||
},
|
||||
"sr": {
|
||||
"nativeName": "српски",
|
||||
"path": "sr/sr"
|
||||
},
|
||||
"sr-Latn": {
|
||||
"nativeName": "srpski",
|
||||
"path": "sr/sr-Latn"
|
||||
},
|
||||
"sv_FI": {
|
||||
"nativeName": "svenska (Finland)",
|
||||
"path": "sv_SE/sv_FI"
|
||||
},
|
||||
"sv_SE": {
|
||||
"nativeName": "svenska (Sverige)",
|
||||
"path": "sv_SE/sv_SE"
|
||||
},
|
||||
"sw_TZ": {
|
||||
"nativeName": "Kiswahili",
|
||||
"path": "sw_TZ/sw_TZ"
|
||||
},
|
||||
"ta_IN": {
|
||||
"nativeName": "தமிழ்",
|
||||
"path": "ta_IN/ta_IN"
|
||||
},
|
||||
"te_IN": {
|
||||
"nativeName": "తెలుగు",
|
||||
"path": "te_IN/te_IN"
|
||||
},
|
||||
"th_TH": {
|
||||
"nativeName": "ไทย",
|
||||
"path": "th_TH/th_TH"
|
||||
},
|
||||
"tr_TR": {
|
||||
"nativeName": "Türkçe",
|
||||
"path": "tr_TR/tr_TR"
|
||||
},
|
||||
"uk_UA": {
|
||||
"nativeName": "українська",
|
||||
"path": "uk_UA/uk_UA"
|
||||
},
|
||||
"vi_VN": {
|
||||
"nativeName": "Tiếng Việt",
|
||||
"path": "vi/vi_VN"
|
||||
}
|
||||
}
|
||||
@ -41,15 +41,20 @@ ApplicationWindow {
|
||||
|
||||
onActiveFocusItemChanged: {
|
||||
focusOverlay.margin = -5;
|
||||
if (activeFocusItem && ((activeFocusItem.focusReason === Qt.TabFocusReason) || (activeFocusItem.focusReason === Qt.BacktabFocusReason))) {
|
||||
if (activeFocusItem.focusOnChild) {
|
||||
focusOverlay.parent = activeFocusItem.parent;
|
||||
} else if (activeFocusItem.dontShowFocusState) {
|
||||
focusOverlay.parent = null;
|
||||
if (activeFocusItem) {
|
||||
const goodReasonToChangeFocus = activeFocusItem instanceof ItemDelegate || ((activeFocusItem.focusReason === Qt.TabFocusReason) || (activeFocusItem.focusReason === Qt.BacktabFocusReason));
|
||||
if (goodReasonToChangeFocus) {
|
||||
if (activeFocusItem.focusOnChild) {
|
||||
focusOverlay.parent = activeFocusItem.parent;
|
||||
} else if (activeFocusItem.dontShowFocusState) {
|
||||
focusOverlay.parent = null;
|
||||
} else {
|
||||
if (activeFocusItem.showFocusMargin)
|
||||
focusOverlay.margin = 0;
|
||||
focusOverlay.parent = activeFocusItem;
|
||||
}
|
||||
} else {
|
||||
if (activeFocusItem.showFocusMargin)
|
||||
focusOverlay.margin = 0;
|
||||
focusOverlay.parent = activeFocusItem;
|
||||
focusOverlay.parent = null;
|
||||
}
|
||||
} else {
|
||||
focusOverlay.parent = null;
|
||||
@ -292,9 +297,9 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
Connections {
|
||||
target: UtilsAdapter
|
||||
function onRaiseWhenCalled() {
|
||||
function onRaiseWhenCalledChanged() {
|
||||
raiseWhenCalled = AppSettingsManager.getValue(Settings.RaiseWhenCalled);
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ extern const QString defaultDownloadPath;
|
||||
X(WindowState, QWindow::AutomaticVisibility) \
|
||||
X(EnableExperimentalSwarm, false) \
|
||||
X(LANG, "SYSTEM") \
|
||||
X(SpellLang, "NONE") \
|
||||
X(SpellLang, {}) \
|
||||
X(EnableSpellCheck, true) \
|
||||
X(PluginStoreEndpoint, "https://plugins.jami.net") \
|
||||
X(PositionShareDuration, 15) \
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
#include "appversionmanager.h"
|
||||
|
||||
#include "lrcinstance.h"
|
||||
#include "version.h"
|
||||
#include "version_info.h"
|
||||
|
||||
#include <QProcess>
|
||||
#include <QTimer>
|
||||
@ -73,7 +73,7 @@ struct AppVersionManager::Impl : public QObject
|
||||
Q_EMIT parent_.updateCheckReplyReceived(false);
|
||||
return;
|
||||
}
|
||||
auto currentVersion = QString(VERSION_STRING).toULongLong();
|
||||
auto currentVersion = BUILD_VERSION_STRING.toULongLong();
|
||||
auto latestVersion = latestVersionString.toULongLong();
|
||||
const QString channelStr = isBeta ? "beta" : "stable";
|
||||
const auto newVersionFound = latestVersion > currentVersion;
|
||||
|
||||
@ -249,7 +249,7 @@ CallAdapter::onCallEnded(const QString& callId)
|
||||
}
|
||||
|
||||
void
|
||||
CallAdapter::onCallStatusChanged(const QString& callId, int code)
|
||||
CallAdapter::onCallStatusChanged(const QString& accountId, const QString& callId, int code)
|
||||
{
|
||||
Q_UNUSED(code)
|
||||
|
||||
@ -468,11 +468,14 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con
|
||||
showNotification(accountId, convInfo.uid);
|
||||
return;
|
||||
}
|
||||
if (!accountProperties.denySecondCall) {
|
||||
lrcInstance_->selectConversation(convInfo.uid, accountId);
|
||||
}
|
||||
} else {
|
||||
// finally, in this case, the conversation isn't selected yet
|
||||
// and there are no other special conditions, so just select the conversation
|
||||
lrcInstance_->selectConversation(convInfo.uid, accountId);
|
||||
}
|
||||
|
||||
// finally, in this case, the conversation isn't selected yet
|
||||
// and there are no other special conditions, so just select the conversation
|
||||
lrcInstance_->selectConversation(convInfo.uid, accountId);
|
||||
}
|
||||
|
||||
void
|
||||
@ -563,7 +566,7 @@ CallAdapter::connectCallModel(const QString& accountId)
|
||||
connect(accInfo.callModel.get(),
|
||||
&CallModel::callStatusChanged,
|
||||
this,
|
||||
QOverload<const QString&, int>::of(&CallAdapter::onCallStatusChanged),
|
||||
QOverload<const QString&, const QString&, int>::of(&CallAdapter::onCallStatusChanged),
|
||||
Qt::UniqueConnection);
|
||||
|
||||
connect(accInfo.callModel.get(),
|
||||
|
||||
@ -118,7 +118,7 @@ public Q_SLOTS:
|
||||
void onShowCallView(const QString& accountId, const QString& convUid);
|
||||
void onAccountChanged();
|
||||
void onCallStatusChanged(const QString& accountId, const QString& callId);
|
||||
void onCallStatusChanged(const QString& callId, int code);
|
||||
void onCallStatusChanged(const QString& accountId, const QString& callId, int code);
|
||||
void onCallAddedToConference(const QString& callId, const QString& conversationId, const QString& confId);
|
||||
void onCallStarted(const QString& callId);
|
||||
void onCallEnded(const QString& callId);
|
||||
|
||||
@ -135,7 +135,7 @@ PendingConferenceesListModel::connectSignals()
|
||||
callsStatusChanged_ = connect(currentCallModel,
|
||||
&CallModel::callStatusChanged,
|
||||
this,
|
||||
[this](const QString&, int) {
|
||||
[this](const QString&, const QString&, int) {
|
||||
Q_EMIT dataChanged(index(0, 0),
|
||||
index(rowCount() - 1),
|
||||
{Role::CallStatus});
|
||||
@ -401,6 +401,16 @@ CallOverlayModel::eventFilter(QObject* object, QEvent* event)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tab or BackTab key events should trigger a signal that we can use to
|
||||
// prevent the overlay from fading and to allow the user to navigate
|
||||
// through the controls.
|
||||
if (event->type() == QEvent::KeyPress && (static_cast<QKeyEvent*>(event)->key() == Qt::Key_Tab)
|
||||
|| (static_cast<QKeyEvent*>(event)->key() == Qt::Key_Backtab)) {
|
||||
Q_EMIT focusKeyPressed();
|
||||
// Don't absorb the event so that the focus can be changed
|
||||
// to the next or previous control.
|
||||
return false;
|
||||
}
|
||||
#ifndef HAVE_GLOBAL_PTT
|
||||
else if (event->type() == QEvent::KeyPress && listener_->getPttState()) {
|
||||
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
|
||||
|
||||
@ -140,6 +140,7 @@ public:
|
||||
|
||||
Q_SIGNALS:
|
||||
void mouseMoved(QQuickItem* item);
|
||||
void focusKeyPressed();
|
||||
void pttKeyPressed();
|
||||
void pttKeyReleased();
|
||||
|
||||
|
||||
@ -78,8 +78,10 @@ Popup {
|
||||
contentItem: ColumnLayout {
|
||||
id: contentLayout
|
||||
|
||||
JamiPushButton {
|
||||
JamiPushButton { QWKSetParentHitTestVisible {}
|
||||
id: closeButton
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: JamiStrings.close
|
||||
|
||||
visible: closeButtonVisible
|
||||
|
||||
@ -117,7 +119,7 @@ Popup {
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredHeight: Math.min(contentHeight, root.height)
|
||||
Layout.preferredWidth: contentItem.childrenRect.width
|
||||
Layout.preferredWidth: contentItem.childrenRect.width + ScrollBar.vertical.width
|
||||
Layout.leftMargin: popupMargins
|
||||
Layout.rightMargin: popupMargins
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
@ -23,12 +23,12 @@ import net.jami.Adapters 1.1
|
||||
import net.jami.Constants 1.1
|
||||
|
||||
SBSMessageBase {
|
||||
id: root
|
||||
id: rootDelegate
|
||||
|
||||
property var confId: ConfId
|
||||
property var currentCallId: CurrentCall.id
|
||||
component JoinCallButton: MaterialButton {
|
||||
visible: root.isActive && root.currentCallId !== root.confId
|
||||
visible: rootDelegate.isActive && rootDelegate.currentCallId !== rootDelegate.confId
|
||||
toolTipText: JamiStrings.joinCall
|
||||
color: JamiTheme.blackColor
|
||||
background.opacity: hovered ? 0.2 : 0.1
|
||||
@ -40,6 +40,20 @@ SBSMessageBase {
|
||||
textRightPadding: 9
|
||||
}
|
||||
|
||||
Accessible.role: Accessible.StaticText
|
||||
Accessible.name: {
|
||||
let name = isOutgoing ? JamiStrings.inReplyToYou : UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author);
|
||||
return name + ": " + callLabel.text + " " + formattedDay;
|
||||
}
|
||||
Accessible.description: {
|
||||
let status = "";
|
||||
if (bubble.isEdited)
|
||||
status += JamiStrings.edited + " ";
|
||||
return status + (readers.length > 0 ? JamiStrings.readBy + " " + readers.map(function (uri) {
|
||||
return UtilsAdapter.getBestNameForUri(CurrentAccount.id, uri);
|
||||
}).join(", ") : "");
|
||||
}
|
||||
|
||||
property bool isRemoteImage
|
||||
|
||||
isOutgoing: Author === CurrentAccount.uri
|
||||
@ -48,17 +62,17 @@ SBSMessageBase {
|
||||
formattedTime: MessagesAdapter.getFormattedTime(Timestamp)
|
||||
|
||||
bubble.border.color: CurrentConversation.color
|
||||
bubble.border.width: root.isActive ? 1.5 : 0
|
||||
bubble.border.width: rootDelegate.isActive ? 1.5 : 0
|
||||
bubble.color: JamiTheme.messageInBgColor
|
||||
bubble.opacity: 1
|
||||
|
||||
Connections {
|
||||
target: CurrentConversation
|
||||
enabled: root.isActive
|
||||
enabled: rootDelegate.isActive
|
||||
|
||||
function onActiveCallsChanged() {
|
||||
root.isActive = LRCInstance.indexOfActiveCall(root.confId, ActionUri, DeviceId) !== -1;
|
||||
if (root.isActive) {
|
||||
rootDelegate.isActive = LRCInstance.indexOfActiveCall(rootDelegate.confId, ActionUri, DeviceId) !== -1;
|
||||
if (rootDelegate.isActive) {
|
||||
bubble.mask.border.color = CurrentConversation.color;
|
||||
bubble.mask.border.width = 1.5;
|
||||
bubble.mask.z = -2;
|
||||
@ -66,8 +80,8 @@ SBSMessageBase {
|
||||
}
|
||||
}
|
||||
|
||||
property bool isActive: LRCInstance.indexOfActiveCall(root.confId, ActionUri, DeviceId) !== -1
|
||||
visible: isActive || root.confId === "" || Duration > 0
|
||||
property bool isActive: LRCInstance.indexOfActiveCall(rootDelegate.confId, ActionUri, DeviceId) !== -1
|
||||
visible: isActive || rootDelegate.confId === "" || Duration > 0
|
||||
|
||||
property var baseColor: JamiTheme.messageInBgColor
|
||||
|
||||
@ -76,7 +90,7 @@ SBSMessageBase {
|
||||
id: msg
|
||||
anchors.right: isOutgoing ? parent.right : undefined
|
||||
spacing: 10
|
||||
visible: root.visible
|
||||
visible: rootDelegate.visible
|
||||
|
||||
Image {
|
||||
id: statusIcon
|
||||
@ -84,10 +98,10 @@ SBSMessageBase {
|
||||
width: 10
|
||||
height: 10
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
visible: !root.isActive
|
||||
visible: !rootDelegate.isActive
|
||||
|
||||
source: {
|
||||
if (root.isOutgoing) {
|
||||
if (rootDelegate.isOutgoing) {
|
||||
if (Duration > 0)
|
||||
return "qrc:/icons/outgoing-call.svg";
|
||||
else
|
||||
@ -104,12 +118,11 @@ SBSMessageBase {
|
||||
effect: ColorOverlay {
|
||||
color: {
|
||||
if (Duration > 0)
|
||||
return UtilsAdapter.luma(root.baseColor) ? JamiTheme.chatviewTextColorLight : JamiTheme.chatviewTextColorDark
|
||||
return JamiTheme.redColor
|
||||
return UtilsAdapter.luma(rootDelegate.baseColor) ? JamiTheme.chatviewTextColorLight : JamiTheme.chatviewTextColorDark;
|
||||
return JamiTheme.redColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
@ -120,11 +133,11 @@ SBSMessageBase {
|
||||
bottomPadding: 8
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: root.isActive && root.currentCallId !== root.confId ? 0 : root.timeWidth + 16
|
||||
Layout.leftMargin: root.isActive ? 10 : -5 /* spacing is 10 and we want 5px with icon */
|
||||
Layout.rightMargin: rootDelegate.isActive && rootDelegate.currentCallId !== rootDelegate.confId ? 0 : rootDelegate.timeWidth + 16
|
||||
Layout.leftMargin: rootDelegate.isActive ? 10 : -5 /* spacing is 10 and we want 5px with icon */
|
||||
|
||||
text: {
|
||||
if (root.isActive)
|
||||
if (rootDelegate.isActive)
|
||||
return JamiStrings.startedACall;
|
||||
return Body;
|
||||
}
|
||||
@ -136,7 +149,7 @@ SBSMessageBase {
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: Text.MarkdownText
|
||||
|
||||
color: UtilsAdapter.luma(root.baseColor) ? JamiTheme.chatviewTextColorLight : JamiTheme.chatviewTextColorDark
|
||||
color: UtilsAdapter.luma(rootDelegate.baseColor) ? JamiTheme.chatviewTextColorLight : JamiTheme.chatviewTextColorDark
|
||||
}
|
||||
|
||||
JoinCallButton {
|
||||
@ -146,7 +159,7 @@ SBSMessageBase {
|
||||
Layout.bottomMargin: 4
|
||||
|
||||
text: JamiStrings.joinWithAudio
|
||||
onClicked: MessagesAdapter.joinCall(ActionUri, DeviceId, root.confId, true)
|
||||
onClicked: MessagesAdapter.joinCall(ActionUri, DeviceId, rootDelegate.confId, true)
|
||||
}
|
||||
|
||||
JoinCallButton {
|
||||
@ -156,20 +169,20 @@ SBSMessageBase {
|
||||
Layout.topMargin: 4
|
||||
Layout.bottomMargin: 4
|
||||
|
||||
onClicked: MessagesAdapter.joinCall(ActionUri, DeviceId, root.confId)
|
||||
onClicked: MessagesAdapter.joinCall(ActionUri, DeviceId, rootDelegate.confId)
|
||||
Layout.rightMargin: 4
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
opacity: 0
|
||||
Behavior on opacity {
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
bubble.timestampItem.visible = !root.isActive || root.currentCallId === root.confId;
|
||||
bubble.timestampItem.visible = !rootDelegate.isActive || rootDelegate.currentCallId === rootDelegate.confId;
|
||||
opacity = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,8 +21,8 @@ import net.jami.Models 1.1
|
||||
import net.jami.Adapters 1.1
|
||||
import net.jami.Constants 1.1
|
||||
|
||||
Column {
|
||||
id: root
|
||||
Control {
|
||||
id: rootDelegate
|
||||
|
||||
property bool showTime: false
|
||||
property bool showDay: false
|
||||
@ -36,21 +36,41 @@ Column {
|
||||
height: timestampItem.height + textLabel.height
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
Accessible.name: {
|
||||
let name = UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author);
|
||||
return name + ": " + Body + " " + formattedTime + " " + formattedDay;
|
||||
}
|
||||
Accessible.description: {
|
||||
let status = "";
|
||||
if (IsLastSent)
|
||||
status += JamiStrings.sent + " ";
|
||||
return status;
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
id: focusIndicator
|
||||
visible: rootDelegate.activeFocus
|
||||
border.color: JamiTheme.tintedBlue
|
||||
border.width: 2
|
||||
radius: 10
|
||||
color: "transparent"
|
||||
z: 1
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: timestampItem.height + textLabel.height
|
||||
|
||||
TimestampInfo {
|
||||
id: timestampItem
|
||||
|
||||
showDay: root.showDay
|
||||
showTime: root.showTime
|
||||
formattedTime: root.formattedTime
|
||||
formattedDay: root.formattedDay
|
||||
showDay: rootDelegate.showDay
|
||||
showTime: rootDelegate.showTime
|
||||
formattedTime: rootDelegate.formattedTime
|
||||
formattedDay: rootDelegate.formattedDay
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
|
||||
}
|
||||
|
||||
Label {
|
||||
@ -67,7 +87,7 @@ Column {
|
||||
}
|
||||
}
|
||||
opacity: 0
|
||||
Behavior on opacity {
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
|
||||
@ -24,20 +24,32 @@ import net.jami.Constants 1.1
|
||||
import net.jami.Adapters 1.1
|
||||
|
||||
Loader {
|
||||
id: root
|
||||
id: rootDelegate
|
||||
|
||||
property var mediaInfo
|
||||
property bool showTime
|
||||
property bool showDay
|
||||
property int timestamp: Timestamp
|
||||
property string formattedTime: MessagesAdapter.getFormattedTime(root.timestamp)
|
||||
property string formattedDay: MessagesAdapter.getFormattedDay(root.timestamp)
|
||||
property string formattedTime: MessagesAdapter.getFormattedTime(rootDelegate.timestamp)
|
||||
property string formattedDay: MessagesAdapter.getFormattedDay(rootDelegate.timestamp)
|
||||
|
||||
property int seq: MsgSeq.single
|
||||
property string author: Author
|
||||
property string body: Body
|
||||
property var tid: TID
|
||||
property int transferStatus: TransferStatus
|
||||
|
||||
Accessible.name: {
|
||||
let name = UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author);
|
||||
return JamiStrings.dataTransfer + name + ": " + JamiStrings.status + TransferStatus + Body + " " + formattedTime + " " + formattedDay;
|
||||
}
|
||||
Accessible.description: {
|
||||
let status = "";
|
||||
if (IsLastSent)
|
||||
status += JamiStrings.sent + " ";
|
||||
return status;
|
||||
}
|
||||
|
||||
onTidChanged: {
|
||||
if (tid === "") {
|
||||
sourceComponent = deletedMsgComp;
|
||||
@ -48,7 +60,7 @@ Loader {
|
||||
sourceComponent = deletedMsgComp;
|
||||
return;
|
||||
} else if (transferStatus === Interaction.TransferStatus.TRANSFER_FINISHED) {
|
||||
mediaInfo = MessagesAdapter.getMediaInfo(root.body);
|
||||
mediaInfo = MessagesAdapter.getMediaInfo(rootDelegate.body);
|
||||
if (Object.keys(mediaInfo).length !== 0 && WITH_WEBENGINE) {
|
||||
sourceComponent = localMediaMsgComp;
|
||||
return;
|
||||
@ -74,13 +86,13 @@ Loader {
|
||||
id: deletedItem
|
||||
|
||||
isOutgoing: Author === CurrentAccount.uri
|
||||
showTime: root.showTime
|
||||
seq: root.seq
|
||||
showTime: rootDelegate.showTime
|
||||
seq: rootDelegate.seq
|
||||
author: Author
|
||||
readers: Readers
|
||||
timestamp: root.timestamp
|
||||
formattedTime: root.formattedTime
|
||||
formattedDay: root.formattedTime
|
||||
timestamp: rootDelegate.timestamp
|
||||
formattedTime: rootDelegate.formattedTime
|
||||
formattedDay: rootDelegate.formattedTime
|
||||
extraHeight: 0
|
||||
textContentWidth: textEditId.width
|
||||
textContentHeight: textEditId.height
|
||||
@ -122,34 +134,34 @@ Loader {
|
||||
id: dataTransferItem
|
||||
|
||||
transferId: Id
|
||||
property var transferStats: MessagesAdapter.getTransferStats(transferId, root.transferStatus)
|
||||
property bool canOpen: root.transferStatus === Interaction.TransferStatus.TRANSFER_FINISHED || isOutgoing
|
||||
property real maxMsgWidth: root.width - senderMargin - 2 * hPadding - avatarBlockWidth - buttonsLoader.width - 24 - 6 - 24
|
||||
property var transferStats: MessagesAdapter.getTransferStats(transferId, rootDelegate.transferStatus)
|
||||
property bool canOpen: rootDelegate.transferStatus === Interaction.TransferStatus.TRANSFER_FINISHED || isOutgoing
|
||||
property real maxMsgWidth: rootDelegate.width - senderMargin - 2 * hPadding - avatarBlockWidth - buttonsLoader.width - 24 - 6 - 24
|
||||
|
||||
// Timer to update the translation bar
|
||||
Loader {
|
||||
id: timerLoader
|
||||
active: root.transferStatus === Interaction.TransferStatus.TRANSFER_ONGOING
|
||||
active: rootDelegate.transferStatus === Interaction.TransferStatus.TRANSFER_ONGOING
|
||||
sourceComponent: Timer {
|
||||
interval: 1000 // Update every second
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
transferStats = MessagesAdapter.getTransferStats(transferId, root.transferStatus);
|
||||
transferStats = MessagesAdapter.getTransferStats(transferId, rootDelegate.transferStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isOutgoing: Author === CurrentAccount.uri
|
||||
showTime: root.showTime
|
||||
seq: root.seq
|
||||
showTime: rootDelegate.showTime
|
||||
seq: rootDelegate.seq
|
||||
author: Author
|
||||
location: Body
|
||||
transferName: TransferName
|
||||
readers: Readers
|
||||
timestamp: root.timestamp
|
||||
formattedTime: root.formattedTime
|
||||
formattedDay: root.formattedTime
|
||||
timestamp: rootDelegate.timestamp
|
||||
formattedTime: rootDelegate.formattedTime
|
||||
formattedDay: rootDelegate.formattedTime
|
||||
extraHeight: progressBar.visible ? 25 : 0
|
||||
|
||||
innerContent.children: [
|
||||
@ -178,7 +190,7 @@ Loader {
|
||||
Layout.margins: 8
|
||||
|
||||
sourceComponent: {
|
||||
switch (root.transferStatus) {
|
||||
switch (rootDelegate.transferStatus) {
|
||||
case Interaction.TransferStatus.TRANSFER_CREATED:
|
||||
case Interaction.TransferStatus.TRANSFER_FINISHED:
|
||||
iconSource = JamiResources.link_black_24dp_svg;
|
||||
@ -225,7 +237,7 @@ Loader {
|
||||
normalColor: JamiTheme.chatviewBgColor
|
||||
imageColor: JamiTheme.chatviewButtonColor
|
||||
onClicked: {
|
||||
if (root.transferStatus === Interaction.TransferStatus.TRANSFER_ONGOING) {
|
||||
if (rootDelegate.transferStatus === Interaction.TransferStatus.TRANSFER_ONGOING) {
|
||||
MessagesAdapter.cancelFile(transferId);
|
||||
} else {
|
||||
buttonsLoader.iconSource = JamiResources.connecting_black_24dp_svg;
|
||||
@ -287,7 +299,7 @@ Loader {
|
||||
ProgressBar {
|
||||
id: progressBar
|
||||
|
||||
visible: root.transferStatus === Interaction.TransferStatus.TRANSFER_ONGOING
|
||||
visible: rootDelegate.transferStatus === Interaction.TransferStatus.TRANSFER_ONGOING
|
||||
height: visible * implicitHeight
|
||||
value: transferStats.progress / transferStats.totalSize
|
||||
width: transferItem.width
|
||||
@ -305,15 +317,15 @@ Loader {
|
||||
|
||||
isOutgoing: Author === CurrentAccount.uri
|
||||
transferId: Id
|
||||
property var transferStats: MessagesAdapter.getTransferStats(transferId, root.transferStatus)
|
||||
showTime: root.showTime
|
||||
seq: root.seq
|
||||
property var transferStats: MessagesAdapter.getTransferStats(transferId, rootDelegate.transferStatus)
|
||||
showTime: rootDelegate.showTime
|
||||
seq: rootDelegate.seq
|
||||
author: Author
|
||||
location: Body
|
||||
transferName: TransferName
|
||||
readers: Readers
|
||||
formattedTime: MessagesAdapter.getFormattedTime(root.timestamp)
|
||||
formattedDay: MessagesAdapter.getFormattedDay(root.timestamp)
|
||||
formattedTime: MessagesAdapter.getFormattedTime(rootDelegate.timestamp)
|
||||
formattedDay: MessagesAdapter.getFormattedDay(rootDelegate.timestamp)
|
||||
|
||||
property real contentWidth
|
||||
|
||||
|
||||
370
src/app/commoncomponents/DictionaryInstallView.qml
Normal file
370
src/app/commoncomponents/DictionaryInstallView.qml
Normal file
@ -0,0 +1,370 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Savoir-faire Linux Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import net.jami.Models 1.1
|
||||
import net.jami.Adapters 1.1
|
||||
import net.jami.Constants 1.1
|
||||
import "../mainview/components"
|
||||
import "../settingsview/components"
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
// Search bar for filtering dictionaries
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
property int checkBoxWidth: 24
|
||||
|
||||
Component.onCompleted: Qt.callLater(dictionarySearchBar.setTextAreaFocus)
|
||||
|
||||
RowLayout {
|
||||
id: headerLayout
|
||||
width: parent.width
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
|
||||
// Header title
|
||||
Searchbar {
|
||||
id: dictionarySearchBar
|
||||
|
||||
focus: true
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 55
|
||||
|
||||
placeHolderText: JamiStrings.searchTextLanguages
|
||||
Accessible.name: JamiStrings.searchTextLanguages
|
||||
Accessible.role: Accessible.EditableText
|
||||
Accessible.description: JamiStrings.searchAvailableTextLanguages
|
||||
|
||||
onSearchBarTextChanged: function (text) {
|
||||
dictionaryProxyModel.combinedFilterPattern = text;
|
||||
dictionaryProxyModel.invalidate();
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: JamiStrings.showInstalledDictionaries
|
||||
color: JamiTheme.faddedLastInteractionFontColor
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize
|
||||
Layout.rightMargin: 0
|
||||
Layout.preferredHeight: 16
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Checkbox to filter installed dictionaries
|
||||
CheckBox {
|
||||
id: showInstalledOnlyCheckbox
|
||||
Accessible.name: JamiStrings.showInstalledDictionaries
|
||||
Accessible.role: Accessible.CheckBox
|
||||
Accessible.description: JamiStrings.showInstalledDictionariesDescription
|
||||
checked: false
|
||||
indicator: Image {
|
||||
anchors.centerIn: parent
|
||||
layer {
|
||||
enabled: true
|
||||
effect: ColorOverlay {
|
||||
color: JamiTheme.tintedBlue
|
||||
}
|
||||
mipmap: false
|
||||
smooth: true
|
||||
}
|
||||
width: checkBoxWidth
|
||||
height: checkBoxWidth
|
||||
source: showInstalledOnlyCheckbox.checked ? JamiResources.check_box_24dp_svg : JamiResources.check_box_outline_blank_24dp_svg
|
||||
}
|
||||
|
||||
Layout.preferredWidth: 55
|
||||
Layout.preferredHeight: 55
|
||||
Layout.rightMargin: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to listen for download failure and pop a simple dialog to inform the user
|
||||
Connections {
|
||||
target: SpellCheckAdapter
|
||||
function onDownloadFailed(locale) {
|
||||
viewCoordinator.presentDialog(appWindow, "commoncomponents/SimpleMessageDialog.qml", {
|
||||
"title": JamiStrings.error,
|
||||
"infoText": JamiStrings.spellCheckDownloadFailed.arg(locale),
|
||||
"buttonTitles": [JamiStrings.optionOk],
|
||||
"buttonStyles": [SimpleMessageDialog.ButtonStyle.TintedBlue],
|
||||
"buttonRoles": [DialogButtonBox.AcceptRole]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
JamiListView {
|
||||
id: spellCheckDictionaryListView
|
||||
|
||||
width: parent.width
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: SortFilterProxyModel {
|
||||
id: dictionaryProxyModel
|
||||
sourceModel: SpellCheckAdapter.getDictionaryListModel()
|
||||
|
||||
property string combinedFilterPattern
|
||||
|
||||
filters: AllOf {
|
||||
AnyOf {
|
||||
// Filter by dictionary name
|
||||
RegExpFilter {
|
||||
roleName: "Locale"
|
||||
pattern: dictionaryProxyModel.combinedFilterPattern
|
||||
caseSensitivity: Qt.CaseInsensitive
|
||||
}
|
||||
// Filter by native name
|
||||
RegExpFilter {
|
||||
roleName: "NativeName"
|
||||
pattern: dictionaryProxyModel.combinedFilterPattern
|
||||
caseSensitivity: Qt.CaseInsensitive
|
||||
}
|
||||
}
|
||||
ValueFilter {
|
||||
roleName: "Installed"
|
||||
value: true
|
||||
enabled: showInstalledOnlyCheckbox.checked
|
||||
}
|
||||
}
|
||||
|
||||
sorters: [
|
||||
// Sort by locale alphabetically
|
||||
RoleSorter {
|
||||
roleName: "Locale"
|
||||
sortOrder: Qt.AscendingOrder
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
readonly property int itemMargins: 20
|
||||
topMargin: itemMargins / 2
|
||||
bottomMargin: itemMargins / 2
|
||||
|
||||
spacing: 8
|
||||
clip: true
|
||||
|
||||
delegate: ItemDelegate {
|
||||
id: dictionaryDelegate
|
||||
width: spellCheckDictionaryListView.width
|
||||
height: Math.max(JamiTheme.preferredFieldHeight, contentLayout.implicitHeight + 32)
|
||||
|
||||
background: Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: parent.width - spellCheckDictionaryListView.itemMargins
|
||||
height: parent.height
|
||||
color: JamiTheme.backgroundColor
|
||||
radius: JamiTheme.primaryRadius
|
||||
border.color: "transparent"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 16
|
||||
|
||||
// Dictionary info
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.leftMargin: 16
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
id: dictionaryName
|
||||
Layout.fillWidth: true
|
||||
text: model.NativeName || ""
|
||||
color: JamiTheme.textColor
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: dictionaryLocale
|
||||
Layout.fillWidth: true
|
||||
text: model.Locale || ""
|
||||
color: JamiTheme.faddedLastInteractionFontColor
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize - 2
|
||||
elide: Text.ElideRight
|
||||
visible: text !== ""
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Installation status and action
|
||||
Item {
|
||||
Layout.preferredWidth: 100
|
||||
Layout.preferredHeight: 32
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: 16
|
||||
|
||||
// Install button for available dictionaries
|
||||
MaterialButton {
|
||||
id: installButton
|
||||
anchors.centerIn: parent
|
||||
width: 100
|
||||
height: 32
|
||||
Accessible.name: dictionaryName.text + " " + JamiStrings.install
|
||||
Accessible.role: Accessible.Button
|
||||
|
||||
text: JamiStrings.install
|
||||
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize - 1
|
||||
font.weight: Font.Medium
|
||||
|
||||
focusPolicy: Qt.StrongFocus
|
||||
KeyNavigation.tab: {
|
||||
try {
|
||||
if (model.index < dictionaryProxyModel.count - 1) {
|
||||
var nextItem = spellCheckDictionaryListView.itemAtIndex(model.index + 1);
|
||||
if (nextItem) {
|
||||
var nextButton = nextItem.findChild("installButton") || nextItem.findChild("uninstallButton");
|
||||
return nextButton || null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug("KeyNavigation error handled:", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onFocusChanged: {
|
||||
if (focus) {
|
||||
spellCheckDictionaryListView.positionViewAtIndex(model.index, ListView.Contain);
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (model.Locale) {
|
||||
SpellCheckAdapter.installDictionary(model.Locale);
|
||||
}
|
||||
}
|
||||
|
||||
visible: !model.Downloading && !model.Installed && model.Locale !== undefined && model.Locale !== ""
|
||||
}
|
||||
|
||||
// Uninstall button for installed dictionaries (not system dictionaries)
|
||||
MaterialButton {
|
||||
id: uninstallButton
|
||||
anchors.centerIn: parent
|
||||
width: 100
|
||||
height: 32
|
||||
|
||||
Accessible.name: dictionaryName.text + " " + JamiStrings.uninstall
|
||||
Accessible.role: Accessible.Button
|
||||
|
||||
text: JamiStrings.uninstall
|
||||
color: "#ff6666"
|
||||
hoveredColor: "#ff9999"
|
||||
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize - 1
|
||||
font.weight: Font.Medium
|
||||
|
||||
focusPolicy: Qt.StrongFocus
|
||||
KeyNavigation.tab: {
|
||||
try {
|
||||
if (model.index < dictionaryProxyModel.count - 1) {
|
||||
var nextItem = spellCheckDictionaryListView.itemAtIndex(model.index + 1);
|
||||
if (nextItem) {
|
||||
var nextButton = nextItem.findChild("installButton") || nextItem.findChild("uninstallButton");
|
||||
return nextButton || null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug("KeyNavigation error handled:", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onFocusChanged: {
|
||||
if (focus) {
|
||||
spellCheckDictionaryListView.positionViewAtIndex(model.index, ListView.Contain);
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (model.Locale) {
|
||||
SpellCheckAdapter.uninstallDictionary(model.Locale);
|
||||
}
|
||||
}
|
||||
|
||||
visible: !model.Downloading && model.Installed && !model.IsSystem && model.Locale !== undefined && model.Locale !== ""
|
||||
}
|
||||
|
||||
// System dictionary indicator
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: JamiStrings.systemDictionary
|
||||
color: JamiTheme.faddedLastInteractionFontColor
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize - 2
|
||||
visible: model.IsSystem
|
||||
}
|
||||
|
||||
// Downloading status indicator
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
visible: model.Downloading
|
||||
running: model.Downloading
|
||||
width: 24
|
||||
height: 24
|
||||
|
||||
// Use a custom animation for better UX
|
||||
Behavior on running {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state for when no dictionaries are found
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: dictionaryProxyModel.count === 0
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 16
|
||||
width: parent.width * 0.8
|
||||
|
||||
// Big books emoji
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "📚"
|
||||
font.pixelSize: 48
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
text: dictionarySearchBar.textContent.length > 0 ? JamiStrings.noDictionariesFoundFor.arg(dictionarySearchBar.textContent) : JamiStrings.noDictionariesAvailable
|
||||
color: JamiTheme.faddedLastInteractionFontColor
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,7 @@ import net.jami.Adapters 1.1
|
||||
import net.jami.Constants 1.1
|
||||
|
||||
Column {
|
||||
id: root
|
||||
id: rootDelegate
|
||||
|
||||
property bool showTime: false
|
||||
property bool showDay: false
|
||||
@ -34,6 +34,18 @@ Column {
|
||||
spacing: 2
|
||||
topPadding: 12
|
||||
bottomPadding: 12
|
||||
|
||||
Accessible.name: {
|
||||
let name = UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author);
|
||||
return name + ": " + Body + " " + formattedTime + " " + formattedDay;
|
||||
}
|
||||
Accessible.description: {
|
||||
let status = "";
|
||||
if (IsLastSent)
|
||||
status += JamiStrings.sent + " ";
|
||||
return status;
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
width: parent.width
|
||||
@ -42,10 +54,10 @@ Column {
|
||||
TimestampInfo {
|
||||
id: timestampItem
|
||||
|
||||
showDay: root.showDay
|
||||
showTime: root.showTime
|
||||
formattedTime: root.formattedTime
|
||||
formattedDay: root.formattedDay
|
||||
showDay: rootDelegate.showDay
|
||||
showTime: rootDelegate.showTime
|
||||
formattedTime: rootDelegate.formattedTime
|
||||
formattedDay: rootDelegate.formattedDay
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
@ -60,7 +72,7 @@ Column {
|
||||
}
|
||||
|
||||
opacity: 0
|
||||
Behavior on opacity {
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
|
||||
@ -28,6 +28,10 @@ Control {
|
||||
property string title: ""
|
||||
property string description: ""
|
||||
|
||||
Accessible.role: Accessible.StaticText
|
||||
Accessible.name: title
|
||||
Accessible.description: description
|
||||
|
||||
width: 190
|
||||
height: infos.implicitHeight
|
||||
|
||||
|
||||
@ -17,6 +17,8 @@
|
||||
import QtQuick
|
||||
import net.jami.Adapters 1.1
|
||||
import net.jami.Constants 1.1
|
||||
import net.jami.Enums 1.1
|
||||
import net.jami.Models 1.1
|
||||
import "contextmenu"
|
||||
import "../mainview"
|
||||
import "../mainview/components"
|
||||
@ -30,28 +32,19 @@ ContextMenuAutoLoader {
|
||||
property var selectionEnd
|
||||
property bool customizePaste: false
|
||||
property bool selectOnly: false
|
||||
property bool checkSpell: false
|
||||
property bool spellCheckEnabled: false
|
||||
property var suggestionList
|
||||
property var menuItemsLength
|
||||
property var language
|
||||
|
||||
signal contextMenuRequirePaste
|
||||
|
||||
SpellLanguageContextMenu {
|
||||
id: spellLanguageContextMenu
|
||||
active: checkSpell
|
||||
active: spellCheckEnabled
|
||||
}
|
||||
|
||||
property list<GeneralMenuItem> menuItems: [
|
||||
GeneralMenuItem {
|
||||
id: copy
|
||||
|
||||
canTrigger: true
|
||||
isActif: lineEditObj.selectedText.length
|
||||
itemName: JamiStrings.copy
|
||||
hasIcon: false
|
||||
onClicked:
|
||||
lineEditObj.copy();
|
||||
},
|
||||
GeneralMenuItem {
|
||||
id: cut
|
||||
|
||||
@ -59,8 +52,16 @@ ContextMenuAutoLoader {
|
||||
isActif: lineEditObj.selectedText.length && !selectOnly
|
||||
itemName: JamiStrings.cut
|
||||
hasIcon: false
|
||||
onClicked:
|
||||
lineEditObj.cut();
|
||||
onClicked: lineEditObj.cut()
|
||||
},
|
||||
GeneralMenuItem {
|
||||
id: copy
|
||||
|
||||
canTrigger: true
|
||||
isActif: lineEditObj.selectedText.length
|
||||
itemName: JamiStrings.copy
|
||||
hasIcon: false
|
||||
onClicked: lineEditObj.copy()
|
||||
},
|
||||
GeneralMenuItem {
|
||||
id: paste
|
||||
@ -76,30 +77,39 @@ ContextMenuAutoLoader {
|
||||
}
|
||||
},
|
||||
GeneralMenuItem {
|
||||
id: language
|
||||
visible: checkSpell
|
||||
canTrigger: checkSpell
|
||||
itemName: JamiStrings.language
|
||||
id: textLanguage
|
||||
canTrigger: spellCheckEnabled && SpellCheckAdapter.installedDictionaryCount > 0
|
||||
itemName: JamiStrings.textLanguage
|
||||
hasIcon: false
|
||||
onClicked: {
|
||||
spellLanguageContextMenu.openMenu();
|
||||
}
|
||||
},
|
||||
GeneralMenuItem {
|
||||
id: manageLanguages
|
||||
itemName: JamiStrings.manageDictionaries
|
||||
canTrigger: spellCheckEnabled
|
||||
hasIcon: false
|
||||
onClicked: {
|
||||
viewCoordinator.presentDialog(appWindow, "commoncomponents/ManageDictionariesDialog.qml");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
ListView {
|
||||
model: ListModel {
|
||||
id: dynamicModel
|
||||
id: suggestionListModel
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: dynamicModel
|
||||
model: suggestionListModel
|
||||
delegate: GeneralMenuItem {
|
||||
id: suggestion
|
||||
|
||||
canTrigger: true
|
||||
isActif: true
|
||||
itemName: model.name
|
||||
bold: true
|
||||
hasIcon: false
|
||||
onClicked: {
|
||||
replaceWord(model.name);
|
||||
@ -117,7 +127,7 @@ ContextMenuAutoLoader {
|
||||
}
|
||||
|
||||
function removeItems() {
|
||||
dynamicModel.remove(0, suggestionList.length);
|
||||
suggestionListModel.clear();
|
||||
suggestionList.length = 0;
|
||||
}
|
||||
|
||||
@ -125,7 +135,7 @@ ContextMenuAutoLoader {
|
||||
menuItemsLength = menuItems.length; // Keep initial number of items for easier removal
|
||||
suggestionList = wordList;
|
||||
for (var i = 0; i < suggestionList.length; ++i) {
|
||||
dynamicModel.append({
|
||||
suggestionListModel.append({
|
||||
"name": suggestionList[i]
|
||||
});
|
||||
}
|
||||
@ -154,7 +164,7 @@ ContextMenuAutoLoader {
|
||||
lineEditObj.select(selectionStart, selectionEnd);
|
||||
}
|
||||
function onClosed() {
|
||||
if (!suggestionList || suggestionList.length == 0) {
|
||||
if (!suggestionList || suggestionList.length === 0) {
|
||||
return;
|
||||
}
|
||||
removeItems();
|
||||
|
||||
@ -16,19 +16,21 @@
|
||||
*/
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import net.jami.Models 1.1
|
||||
import net.jami.Adapters 1.1
|
||||
import net.jami.Constants 1.1
|
||||
import net.jami.Helpers 1.1
|
||||
import net.jami.Models 1.1
|
||||
import "../../commoncomponents"
|
||||
import "../commoncomponents/contextmenu"
|
||||
|
||||
Item {
|
||||
id: cachedFile
|
||||
property string dictionaryPath: SpellCheckDictionaryManager.getDictionariesPath()
|
||||
BaseModalDialog {
|
||||
id: root
|
||||
objectName: "manageDictionariesDialog"
|
||||
|
||||
function updateDictionnary(languagePath) {
|
||||
var file = dictionaryPath + languagePath;
|
||||
MessagesAdapter.updateDictionnary(file);
|
||||
title: JamiStrings.manageDictionaries
|
||||
|
||||
popupContent: DictionaryInstallView {
|
||||
Accessible.name: JamiStrings.manageDictionaries
|
||||
Accessible.role: Accessible.PopupMenu
|
||||
width: 400
|
||||
height: 500
|
||||
}
|
||||
}
|
||||
@ -20,14 +20,11 @@ import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Qt.labs.platform
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
import net.jami.Models 1.1
|
||||
import net.jami.Adapters 1.1
|
||||
import net.jami.Constants 1.1
|
||||
|
||||
import "../mainview/components"
|
||||
|
||||
|
||||
BaseModalDialog {
|
||||
id: root
|
||||
|
||||
@ -36,26 +33,27 @@ BaseModalDialog {
|
||||
property real buttonSize: 36
|
||||
property real imageSize: 25
|
||||
|
||||
|
||||
signal focusOnPreviousItem
|
||||
signal focusOnNextItem
|
||||
signal imageValidated
|
||||
signal imageTemporaryValidated
|
||||
signal imageRemoved
|
||||
signal imageTemporaryRemoved
|
||||
|
||||
function startBooth() {
|
||||
recordBox.openRecorder(true)
|
||||
recordBox.openRecorder(true);
|
||||
}
|
||||
|
||||
function stopBooth(){
|
||||
recordBox.closeRecorder()
|
||||
function stopBooth() {
|
||||
recordBox.closeRecorder();
|
||||
}
|
||||
|
||||
function focusOnNextPhotoBoothItem () {
|
||||
takePhotoButton.forceActiveFocus()
|
||||
function focusOnNextPhotoBoothItem() {
|
||||
takePhotoButton.forceActiveFocus();
|
||||
}
|
||||
|
||||
function focusOnPreviousPhotoBoothItem () {
|
||||
importButton.forceActiveFocus()
|
||||
function focusOnPreviousPhotoBoothItem() {
|
||||
importButton.forceActiveFocus();
|
||||
}
|
||||
|
||||
title: JamiStrings.selectImage
|
||||
@ -69,171 +67,165 @@ BaseModalDialog {
|
||||
isPhoto: true
|
||||
visible: false
|
||||
|
||||
onValidatePhoto: function(photo) {
|
||||
if (!root.newItem)
|
||||
AccountAdapter.setCurrentAccountAvatarBase64(photo)
|
||||
else{
|
||||
onValidatePhoto: function (photo) {
|
||||
if (!root.newItem) {
|
||||
AccountAdapter.setCurrentAccountAvatarBase64(photo);
|
||||
imageTemporaryValidated();
|
||||
} else {
|
||||
UtilsAdapter.setTempCreationImageFromString(photo, imageId);
|
||||
imageValidated();
|
||||
}
|
||||
root.close()
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
popupContent: RowLayout {
|
||||
id: buttonsRowLayout
|
||||
id: buttonsRowLayout
|
||||
|
||||
spacing: 18
|
||||
spacing: 18
|
||||
|
||||
JamiPushButton {
|
||||
id: takePhotoButton
|
||||
JamiPushButton {
|
||||
id: takePhotoButton
|
||||
Accessible.name: objectName
|
||||
|
||||
objectName: "takePhotoButton"
|
||||
objectName: "takePhotoButton"
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
height: buttonSize
|
||||
width: buttonSize
|
||||
height: buttonSize
|
||||
width: buttonSize
|
||||
|
||||
enabled: VideoDevices.listSize !== 0
|
||||
hoverEnabled: enabled
|
||||
enabled: VideoDevices.listSize !== 0
|
||||
hoverEnabled: enabled
|
||||
|
||||
normalColor: "transparent"
|
||||
imageColor: hovered ? JamiTheme.textColor : JamiTheme.buttonTintedGreyHovered
|
||||
toolTipText: JamiStrings.takePhoto
|
||||
source: JamiResources.add_a_photo_black_24dp_svg
|
||||
normalColor: "transparent"
|
||||
imageColor: hovered ? JamiTheme.textColor : JamiTheme.buttonTintedGreyHovered
|
||||
toolTipText: JamiStrings.takePhoto
|
||||
source: JamiResources.add_a_photo_black_24dp_svg
|
||||
|
||||
Keys.onPressed: function (keyEvent) {
|
||||
if (keyEvent.key === Qt.Key_Enter ||
|
||||
keyEvent.key === Qt.Key_Return) {
|
||||
clicked()
|
||||
keyEvent.accepted = true
|
||||
} else if (keyEvent.key === Qt.Key_Up) {
|
||||
root.focusOnPreviousItem()
|
||||
keyEvent.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
KeyNavigation.tab: {
|
||||
if (clearButton.visible)
|
||||
return clearButton
|
||||
return importButton
|
||||
}
|
||||
KeyNavigation.down: KeyNavigation.tab
|
||||
|
||||
onClicked: {
|
||||
recordBox.parent = buttonsRowLayout
|
||||
startBooth()
|
||||
Keys.onPressed: function (keyEvent) {
|
||||
if (keyEvent.key === Qt.Key_Enter || keyEvent.key === Qt.Key_Return) {
|
||||
clicked();
|
||||
keyEvent.accepted = true;
|
||||
} else if (keyEvent.key === Qt.Key_Up) {
|
||||
root.focusOnPreviousItem();
|
||||
keyEvent.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
JamiPushButton {
|
||||
id: importButton
|
||||
KeyNavigation.tab: {
|
||||
if (clearButton.visible)
|
||||
return clearButton;
|
||||
return importButton;
|
||||
}
|
||||
KeyNavigation.down: KeyNavigation.tab
|
||||
|
||||
objectName: "photoboothViewImportButton"
|
||||
onClicked: {
|
||||
recordBox.parent = buttonsRowLayout;
|
||||
startBooth();
|
||||
}
|
||||
}
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: parent.visible
|
||||
JamiPushButton {
|
||||
id: importButton
|
||||
|
||||
height: buttonSize
|
||||
width: buttonSize
|
||||
objectName: "photoboothViewImportButton"
|
||||
|
||||
normalColor: "transparent"
|
||||
source: JamiResources.add_photo_alternate_black_24dp_svg
|
||||
imageColor: hovered ? JamiTheme.textColor : JamiTheme.buttonTintedGreyHovered
|
||||
toolTipText: JamiStrings.importFromFile
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: parent.visible
|
||||
|
||||
Keys.onPressed: function (keyEvent) {
|
||||
if (keyEvent.key === Qt.Key_Enter ||
|
||||
keyEvent.key === Qt.Key_Return) {
|
||||
clicked()
|
||||
keyEvent.accepted = true
|
||||
} else if (keyEvent.key === Qt.Key_Down ||
|
||||
keyEvent.key === Qt.Key_Tab) {
|
||||
clearButton.forceActiveFocus()
|
||||
keyEvent.accepted = true
|
||||
}
|
||||
height: buttonSize
|
||||
width: buttonSize
|
||||
|
||||
Accessible.name: objectName
|
||||
|
||||
normalColor: "transparent"
|
||||
source: JamiResources.add_photo_alternate_black_24dp_svg
|
||||
imageColor: hovered ? JamiTheme.textColor : JamiTheme.buttonTintedGreyHovered
|
||||
toolTipText: JamiStrings.importFromFile
|
||||
|
||||
Keys.onPressed: function (keyEvent) {
|
||||
if (keyEvent.key === Qt.Key_Enter || keyEvent.key === Qt.Key_Return) {
|
||||
clicked();
|
||||
keyEvent.accepted = true;
|
||||
} else if (keyEvent.key === Qt.Key_Down || keyEvent.key === Qt.Key_Tab) {
|
||||
clearButton.forceActiveFocus();
|
||||
keyEvent.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
KeyNavigation.up: takePhotoButton
|
||||
KeyNavigation.up: takePhotoButton
|
||||
|
||||
onClicked: {
|
||||
stopBooth()
|
||||
var dlg = viewCoordinator.presentDialog(
|
||||
appWindow,
|
||||
"commoncomponents/JamiFileDialog.qml",
|
||||
{
|
||||
title: JamiStrings.selectProfilePicture,
|
||||
fileMode: JamiFileDialog.OpenFile,
|
||||
folder: StandardPaths.writableLocation(
|
||||
StandardPaths.PicturesLocation),
|
||||
nameFilters: [JamiStrings.imageFiles,
|
||||
JamiStrings.allFiles]
|
||||
})
|
||||
dlg.fileAccepted.connect(function(file) {
|
||||
var filePath = UtilsAdapter.getAbsPath(file)
|
||||
onClicked: {
|
||||
stopBooth();
|
||||
var dlg = viewCoordinator.presentDialog(appWindow, "commoncomponents/JamiFileDialog.qml", {
|
||||
title: JamiStrings.selectProfilePicture,
|
||||
fileMode: JamiFileDialog.OpenFile,
|
||||
folder: StandardPaths.writableLocation(StandardPaths.PicturesLocation),
|
||||
nameFilters: [JamiStrings.imageFiles, JamiStrings.allFiles]
|
||||
});
|
||||
dlg.fileAccepted.connect(function (file) {
|
||||
var filePath = UtilsAdapter.getAbsPath(file);
|
||||
if (!root.newItem) {
|
||||
AccountAdapter.setCurrentAccountAvatarFile(filePath)
|
||||
AccountAdapter.setCurrentAccountAvatarFile(filePath);
|
||||
imageTemporaryValidated();
|
||||
} else {
|
||||
UtilsAdapter.setTempCreationImageFromFile(filePath, root.imageId);
|
||||
imageValidated();
|
||||
}
|
||||
root.close()
|
||||
})
|
||||
root.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
JamiPushButton {
|
||||
id: clearButton
|
||||
|
||||
objectName: "photoboothViewClearButton"
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
height: buttonSize
|
||||
width: buttonSize
|
||||
|
||||
normalColor: "transparent"
|
||||
source: JamiResources.remove_circle_outline_black_24dp_svg
|
||||
toolTipText: JamiStrings.removeImage
|
||||
imageColor: hovered ? JamiTheme.textColor : JamiTheme.buttonTintedGreyHovered
|
||||
|
||||
visible: {
|
||||
if (!newItem && LRCInstance.currentAccountAvatarSet)
|
||||
return true;
|
||||
if (newItem && UtilsAdapter.tempCreationImage(imageId).length !== 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
KeyNavigation.up: importButton
|
||||
|
||||
Keys.onPressed: function (keyEvent) {
|
||||
if (keyEvent.key === Qt.Key_Enter || keyEvent.key === Qt.Key_Return) {
|
||||
clicked();
|
||||
importButton.forceActiveFocus();
|
||||
keyEvent.accepted = true;
|
||||
} else if (keyEvent.key === Qt.Key_Down || keyEvent.key === Qt.Key_Tab) {
|
||||
btnCancel.forceActiveFocus();
|
||||
keyEvent.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
JamiPushButton {
|
||||
id: clearButton
|
||||
|
||||
objectName: "photoboothViewClearButton"
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
height: buttonSize
|
||||
width: buttonSize
|
||||
|
||||
normalColor: "transparent"
|
||||
source: JamiResources.remove_circle_outline_black_24dp_svg
|
||||
toolTipText: JamiStrings.removeImage
|
||||
imageColor: hovered ? JamiTheme.textColor : JamiTheme.buttonTintedGreyHovered
|
||||
|
||||
visible: {
|
||||
if (!newItem && LRCInstance.currentAccountAvatarSet)
|
||||
return true
|
||||
if (newItem && UtilsAdapter.tempCreationImage(imageId).length !== 0)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
KeyNavigation.up: importButton
|
||||
|
||||
Keys.onPressed: function (keyEvent) {
|
||||
if (keyEvent.key === Qt.Key_Enter ||
|
||||
keyEvent.key === Qt.Key_Return) {
|
||||
clicked()
|
||||
importButton.forceActiveFocus()
|
||||
keyEvent.accepted = true
|
||||
} else if (keyEvent.key === Qt.Key_Down ||
|
||||
keyEvent.key === Qt.Key_Tab) {
|
||||
btnCancel.forceActiveFocus()
|
||||
keyEvent.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (!root.newItem)
|
||||
AccountAdapter.setCurrentAccountAvatarBase64()
|
||||
else {
|
||||
UtilsAdapter.setTempCreationImageFromString("", imageId);
|
||||
imageRemoved();
|
||||
}
|
||||
visible = false
|
||||
stopBooth()
|
||||
root.close()
|
||||
onClicked: {
|
||||
if (!root.newItem) {
|
||||
AccountAdapter.setCurrentAccountAvatarBase64();
|
||||
imageTemporaryRemoved();
|
||||
} else {
|
||||
UtilsAdapter.setTempCreationImageFromString("", imageId);
|
||||
imageRemoved();
|
||||
}
|
||||
visible = false;
|
||||
stopBooth();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -37,12 +37,16 @@ Row {
|
||||
|
||||
SystemButton {
|
||||
id: minButton
|
||||
Accessible.name: JamiStrings.minimize
|
||||
Accessible.role: Accessible.Button
|
||||
source: JamiResources.window_bar_minimize_svg
|
||||
onClicked: appWindow.showMinimized()
|
||||
}
|
||||
|
||||
SystemButton {
|
||||
id: maxButton
|
||||
Accessible.name: JamiStrings.maximize
|
||||
Accessible.role: Accessible.Button
|
||||
source: appWindow.visibility === Window.Maximized ?
|
||||
JamiResources.window_bar_restore_svg :
|
||||
JamiResources.window_bar_maximize_svg
|
||||
@ -53,6 +57,8 @@ Row {
|
||||
|
||||
SystemButton {
|
||||
id: closeButton
|
||||
Accessible.name: JamiStrings.closeApplication
|
||||
Accessible.role: Accessible.Button
|
||||
source: JamiResources.window_bar_close_svg
|
||||
baseColor: "#e81123"
|
||||
onClicked: appWindow.close()
|
||||
|
||||
@ -24,6 +24,7 @@ import net.jami.Constants 1.1
|
||||
|
||||
Control {
|
||||
id: root
|
||||
Accessible.role: Accessible.StaticText
|
||||
|
||||
property alias avatarBlockWidth: avatarBlock.width
|
||||
property alias innerContent: innerContent
|
||||
@ -64,6 +65,7 @@ Control {
|
||||
property bool bigMsg
|
||||
property bool timeUnderBubble: false
|
||||
property var type: Type
|
||||
property var shouldBeVisible: msgRowlayout.msgHovered || root.activeFocus || reply.activeFocus || more.activeFocus || share.activeFocus
|
||||
|
||||
// If the ListView attached properties are not available,
|
||||
// then the root delegate is likely a Loader.
|
||||
@ -81,6 +83,16 @@ Control {
|
||||
rightPadding: hPadding
|
||||
leftPadding: hPadding
|
||||
|
||||
background: Rectangle {
|
||||
id: focusIndicator
|
||||
visible: rootDelegate.activeFocus
|
||||
radius: 4
|
||||
border.color: JamiTheme.tintedBlue
|
||||
border.width: 2
|
||||
color: "transparent"
|
||||
z: 1
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: mainColumnLayout
|
||||
|
||||
@ -142,6 +154,7 @@ Control {
|
||||
RowLayout {
|
||||
id: replyToLayout
|
||||
|
||||
spacing: replyItem.isSelf ? 2 : 4
|
||||
Layout.alignment: isOutgoing ? Qt.AlignRight : Qt.AlignLeft
|
||||
property var replyUserName: UtilsAdapter.getBestNameForUri(CurrentAccount.id, ReplyToAuthor)
|
||||
|
||||
@ -185,7 +198,7 @@ Control {
|
||||
text: textMetricsUsername2.elidedText
|
||||
TextMetrics {
|
||||
id: textMetricsUsername2
|
||||
text: replyItem.isSelf ? JamiStrings.inReplyToMe : replyToLayout.replyUserName
|
||||
text: replyItem.isSelf ? JamiStrings.inReplyToYou : replyToLayout.replyUserName
|
||||
elideWidth: 200
|
||||
elide: Qt.ElideMiddle
|
||||
}
|
||||
@ -300,7 +313,7 @@ Control {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: isOutgoing ? optionButtonItem.right : undefined
|
||||
anchors.left: !isOutgoing ? optionButtonItem.left : undefined
|
||||
visible: msgRowlayout.msgHovered
|
||||
visible: shouldBeVisible
|
||||
source: JamiResources.more_vert_24dp_svg
|
||||
width: optionButtonItem.width / 4
|
||||
height: optionButtonItem.height
|
||||
@ -310,7 +323,7 @@ Control {
|
||||
|
||||
function setBindings() {
|
||||
more.isOpen = false;
|
||||
visible = Qt.binding(() => msgRowlayout.msgHovered);
|
||||
visible = Qt.binding(() => shouldBeVisible);
|
||||
imageColor = Qt.binding(() => hovered ? JamiTheme.chatViewFooterImgHoverColor : JamiTheme.chatViewFooterImgColor);
|
||||
normalColor = Qt.binding(() => JamiTheme.primaryBackgroundColor);
|
||||
}
|
||||
@ -355,7 +368,7 @@ Control {
|
||||
anchors.rightMargin: 5
|
||||
anchors.right: isOutgoing ? more.left : undefined
|
||||
anchors.left: !isOutgoing ? more.right : undefined
|
||||
visible: msgRowlayout.msgHovered
|
||||
visible: shouldBeVisible
|
||||
|
||||
onClicked: {
|
||||
MessagesAdapter.editId = "";
|
||||
@ -379,13 +392,14 @@ Control {
|
||||
anchors.rightMargin: 5
|
||||
anchors.right: isOutgoing ? reply.left : undefined
|
||||
anchors.left: !isOutgoing ? reply.right : undefined
|
||||
visible: msgRowlayout.msgHovered
|
||||
|
||||
visible: shouldBeVisible
|
||||
property bool isOpen: false
|
||||
property var obj: undefined
|
||||
|
||||
function setBindings() { // when the popup is closed, setBindings is called to reset the icon's visual settings
|
||||
share.isOpen = false;
|
||||
visible = Qt.binding(() => msgRowlayout.msgHovered);
|
||||
visible = Qt.binding(() => shouldBeVisible);
|
||||
imageColor = Qt.binding(() => hovered ? JamiTheme.chatViewFooterImgHoverColor : JamiTheme.chatViewFooterImgColor);
|
||||
normalColor = Qt.binding(() => JamiTheme.primaryBackgroundColor);
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ import QtQuick.Layouts
|
||||
Rectangle {
|
||||
property alias name: label.text
|
||||
property bool stretchParent: false
|
||||
property string tag: parent.toString()
|
||||
property string tag: parent.toString() + " (w:" + width + ", h: " + height + ")"
|
||||
signal moveX(real dx)
|
||||
signal moveY(real dy)
|
||||
property real ox: 0
|
||||
|
||||
@ -50,11 +50,14 @@ ComboBox {
|
||||
|
||||
contentItem: Text {
|
||||
text: {
|
||||
if (index < 0)
|
||||
if (index < 0 || !model)
|
||||
return '';
|
||||
var currentItem = root.delegateModel.items.get(index);
|
||||
const value = currentItem.model[root.textRole];
|
||||
return value === undefined ? '' : value.toString();
|
||||
|
||||
if (root.textRole && model[root.textRole] !== undefined) {
|
||||
return model[root.textRole].toString();
|
||||
}
|
||||
|
||||
return model.display !== undefined ? model.display.toString() : '';
|
||||
}
|
||||
|
||||
color: hovered ? JamiTheme.comboboxTextColorHovered : JamiTheme.textColor
|
||||
@ -80,7 +83,7 @@ ComboBox {
|
||||
|
||||
source: popup.visible ? JamiResources.expand_less_24dp_svg : JamiResources.expand_more_24dp_svg
|
||||
|
||||
color: JamiTheme.comboboxIconColor
|
||||
color: root.enabled ? JamiTheme.comboboxIconColor : "grey"
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
@ -92,7 +95,7 @@ ComboBox {
|
||||
anchors.rightMargin: root.indicator.width * 2
|
||||
font.pixelSize: JamiTheme.settingsDescriptionPixelSize
|
||||
text: root.displayText
|
||||
color: JamiTheme.comboboxTextColor
|
||||
color: root.enabled ? JamiTheme.comboboxTextColor : "grey"
|
||||
font.weight: Font.Medium
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
@ -104,7 +107,11 @@ ComboBox {
|
||||
color: JamiTheme.transparentColor
|
||||
implicitWidth: 120
|
||||
implicitHeight: contentItem.implicitHeight + JamiTheme.buttontextHeightMargin
|
||||
border.color: popup.visible ? JamiTheme.comboboxBorderColorActive : JamiTheme.comboboxBorderColor
|
||||
border.color: root.enabled ?
|
||||
(popup.visible ?
|
||||
JamiTheme.comboboxBorderColorActive :
|
||||
JamiTheme.comboboxBorderColor) :
|
||||
"grey"
|
||||
border.width: root.visualFocus ? 2 : 1
|
||||
radius: 5
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import "contextmenu"
|
||||
|
||||
BaseContextMenu {
|
||||
id: root
|
||||
|
||||
property var modelList
|
||||
signal audioRecordMessageButtonClicked
|
||||
signal videoRecordMessageButtonClicked
|
||||
@ -31,35 +32,63 @@ BaseContextMenu {
|
||||
GeneralMenuItem {
|
||||
id: audioMessage
|
||||
|
||||
Accessible.role: Accessible.MenuItem
|
||||
Accessible.name: itemName
|
||||
focusPolicy: Qt.StrongFocus
|
||||
Keys.onReturnPressed: clicked()
|
||||
|
||||
canTrigger: true
|
||||
iconSource: JamiResources.message_audio_black_24dp_svg
|
||||
itemName: JamiStrings.leaveAudioMessage
|
||||
onClicked: {
|
||||
root.audioRecordMessageButtonClicked();
|
||||
root.close()
|
||||
}
|
||||
|
||||
KeyNavigation.tab: videoMessage
|
||||
KeyNavigation.backtab: shareLocation
|
||||
},
|
||||
GeneralMenuItem {
|
||||
id: videoMessage
|
||||
|
||||
Accessible.role: Accessible.MenuItem
|
||||
Accessible.name: itemName
|
||||
|
||||
focusPolicy: Qt.StrongFocus
|
||||
Keys.onReturnPressed: clicked()
|
||||
|
||||
canTrigger: true
|
||||
iconSource: JamiResources.message_video_black_24dp_svg
|
||||
itemName: JamiStrings.leaveVideoMessage
|
||||
|
||||
isActif: VideoDevices.listSize !== 0
|
||||
|
||||
onClicked: {
|
||||
root.videoRecordMessageButtonClicked();
|
||||
root.close()
|
||||
}
|
||||
|
||||
KeyNavigation.tab: shareLocation
|
||||
KeyNavigation.backtab: audioMessage
|
||||
},
|
||||
GeneralMenuItem {
|
||||
id: shareLocation
|
||||
|
||||
Accessible.role: Accessible.MenuItem
|
||||
Accessible.name: itemName
|
||||
|
||||
focusPolicy: Qt.StrongFocus
|
||||
Keys.onReturnPressed: clicked()
|
||||
|
||||
canTrigger: true
|
||||
iconSource: JamiResources.localisation_sharing_send_pin_svg
|
||||
itemName: JamiStrings.shareLocation
|
||||
onClicked: {
|
||||
root.showMapClicked();
|
||||
root.close()
|
||||
}
|
||||
|
||||
KeyNavigation.tab: audioMessage
|
||||
KeyNavigation.backtab: videoMessage
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@ -22,15 +22,12 @@ import net.jami.Enums 1.1
|
||||
import "contextmenu"
|
||||
import "../mainview"
|
||||
import "../mainview/components"
|
||||
import SortFilterProxyModel 0.2
|
||||
|
||||
ContextMenuAutoLoader {
|
||||
id: root
|
||||
|
||||
signal languageChanged()
|
||||
|
||||
CachedFile {
|
||||
id: cachedFile
|
||||
}
|
||||
signal languageChanged
|
||||
|
||||
function openMenuAt(mouseEvent) {
|
||||
x = mouseEvent.x;
|
||||
@ -46,9 +43,11 @@ ContextMenuAutoLoader {
|
||||
function generateMenuItems() {
|
||||
var menuItems = [];
|
||||
// Create new menu items
|
||||
var dictionaries = SpellCheckDictionaryManager.installedDictionaries();
|
||||
var dictionaries = SpellCheckAdapter.getInstalledDictionaries();
|
||||
var keys = Object.keys(dictionaries);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
const locale = keys[i];
|
||||
const nativeName = dictionaries[keys[i]];
|
||||
var menuItem = Qt.createComponent("qrc:/commoncomponents/contextmenu/GeneralMenuItem.qml", Component.PreferSynchronous);
|
||||
if (menuItem.status !== Component.Ready) {
|
||||
console.error("Error loading component:", menuItem.errorString());
|
||||
@ -58,17 +57,19 @@ ContextMenuAutoLoader {
|
||||
"parent": root,
|
||||
"canTrigger": true,
|
||||
"isActif": true,
|
||||
"itemName": dictionaries[keys[i]],
|
||||
"itemName": nativeName,
|
||||
"hasIcon": false,
|
||||
"content": keys[i],
|
||||
"content": locale,
|
||||
"bold": UtilsAdapter.getAppValue(Settings.SpellLang) === locale
|
||||
});
|
||||
if (menuItemObject === null) {
|
||||
console.error("Error creating menu item:", menuItem.errorString());
|
||||
continue;
|
||||
}
|
||||
menuItemObject.clicked.connect(function () {
|
||||
UtilsAdapter.setAppValue(Settings.Key.SpellLang, menuItemObject.content);
|
||||
});
|
||||
const locale = menuItemObject.content;
|
||||
SpellCheckAdapter.setDictionary(locale);
|
||||
});
|
||||
// Log the object pointer
|
||||
menuItems.push(menuItemObject);
|
||||
}
|
||||
|
||||
@ -24,7 +24,21 @@ import net.jami.Constants 1.1
|
||||
import net.jami.Enums 1.1
|
||||
|
||||
SBSMessageBase {
|
||||
id: root
|
||||
id: rootDelegate
|
||||
|
||||
Accessible.role: Accessible.StaticText
|
||||
Accessible.name: {
|
||||
let name = isOutgoing ? JamiStrings.inReplyToYou : UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author);
|
||||
return name + ": " + Body + " " + formattedTime;
|
||||
}
|
||||
Accessible.description: {
|
||||
let status = "";
|
||||
if (bubble.isEdited)
|
||||
status += JamiStrings.edited + " ";
|
||||
return status + (readers.length > 0 ? JamiStrings.readBy + " " + readers.map(function (uri) {
|
||||
return UtilsAdapter.getBestNameForUri(CurrentAccount.id, uri);
|
||||
}).join(", ") : "");
|
||||
}
|
||||
|
||||
property bool isRemoteImage
|
||||
property bool isEmojiOnly: IsEmojiOnly
|
||||
@ -34,11 +48,11 @@ SBSMessageBase {
|
||||
Connections {
|
||||
target: bubble
|
||||
function onColorChanged(color) {
|
||||
root.colorUrl = UtilsAdapter.luma(bubble.color) ? JamiTheme.chatviewLinkColorLight : JamiTheme.chatviewLinkColorDark;
|
||||
root.colorText = UtilsAdapter.luma(bubble.color) ? JamiTheme.chatviewTextColorLight : JamiTheme.chatviewTextColorDark;
|
||||
rootDelegate.colorUrl = UtilsAdapter.luma(bubble.color) ? JamiTheme.chatviewLinkColorLight : JamiTheme.chatviewLinkColorDark;
|
||||
rootDelegate.colorText = UtilsAdapter.luma(bubble.color) ? JamiTheme.chatviewTextColorLight : JamiTheme.chatviewTextColorDark;
|
||||
// Update parsed body with correct colors
|
||||
if (Body !== "")
|
||||
MessagesAdapter.parseMessage(Id, Body, UtilsAdapter.getAppValue(Settings.DisplayHyperlinkPreviews), root.colorUrl, bubble.color);
|
||||
MessagesAdapter.parseMessage(Id, Body, UtilsAdapter.getAppValue(Settings.DisplayHyperlinkPreviews), rootDelegate.colorUrl, bubble.color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,7 +67,7 @@ SBSMessageBase {
|
||||
textContentWidth: textEditId.width
|
||||
textContentHeight: textEditId.height
|
||||
|
||||
bigMsg: textContentWidth >= (2 / 3) * root.maxMsgWidth || extraContent.active
|
||||
bigMsg: textContentWidth >= (2 / 3) * rootDelegate.maxMsgWidth || extraContent.active
|
||||
|
||||
innerContent.children: [
|
||||
TextEdit {
|
||||
@ -63,10 +77,10 @@ SBSMessageBase {
|
||||
topPadding: bubble.isDeleted ? 6 : 10
|
||||
bottomPadding: bubble.isDeleted ? 6 : 10
|
||||
anchors.right: isOutgoing ? parent.right : undefined
|
||||
anchors.rightMargin: isOutgoing && !isEmojiOnly && !bigMsg ? root.timeWidth + root.editedWidth : 0
|
||||
anchors.rightMargin: isOutgoing && !isEmojiOnly && !bigMsg ? rootDelegate.timeWidth + rootDelegate.editedWidth : 0
|
||||
text: {
|
||||
if (Body !== "" && ParsedBody.length === 0) {
|
||||
MessagesAdapter.parseMessage(Id, Body, UtilsAdapter.getAppValue(Settings.DisplayHyperlinkPreviews), root.colorUrl, bubble.color);
|
||||
MessagesAdapter.parseMessage(Id, Body, UtilsAdapter.getAppValue(Settings.DisplayHyperlinkPreviews), rootDelegate.colorUrl, bubble.color);
|
||||
return "";
|
||||
}
|
||||
if (ParsedBody !== "")
|
||||
@ -82,11 +96,11 @@ SBSMessageBase {
|
||||
|
||||
width: {
|
||||
if (extraContent.active)
|
||||
Math.max(extraContent.width, Math.min((2 / 3) * root.maxMsgWidth, implicitWidth - avatarBlockWidth, extraContent.minSize) - senderMargin);
|
||||
Math.max(extraContent.width, Math.min((2 / 3) * rootDelegate.maxMsgWidth, implicitWidth - avatarBlockWidth, extraContent.minSize) - senderMargin);
|
||||
else if (isEmojiOnly)
|
||||
Math.min((2 / 3) * root.maxMsgWidth, implicitWidth, innerContent.width - senderMargin - (innerContent.width - senderMargin) % (JamiTheme.chatviewEmojiSize + 2));
|
||||
Math.min((2 / 3) * rootDelegate.maxMsgWidth, implicitWidth, innerContent.width - senderMargin - (innerContent.width - senderMargin) % (JamiTheme.chatviewEmojiSize + 2));
|
||||
else
|
||||
Math.min((2 / 3) * root.maxMsgWidth, implicitWidth + 5, innerContent.width - senderMargin + 5);
|
||||
Math.min((2 / 3) * rootDelegate.maxMsgWidth, implicitWidth + 5, innerContent.width - senderMargin + 5);
|
||||
}
|
||||
|
||||
wrapMode: Label.WrapAtWordBoundaryOrAnywhere
|
||||
@ -96,7 +110,7 @@ SBSMessageBase {
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: Text.RichText
|
||||
clip: true
|
||||
onLinkHovered: root.hoveredLink = hoveredLink
|
||||
onLinkHovered: rootDelegate.hoveredLink = hoveredLink
|
||||
onLinkActivated: Qt.openUrlExternally(new URL(hoveredLink))
|
||||
readOnly: true
|
||||
color: (ParsedBody !== "") ? getBaseColor() : (UtilsAdapter.luma(bubble.color) ? "white" : "dark")
|
||||
@ -150,7 +164,7 @@ SBSMessageBase {
|
||||
HoverHandler {
|
||||
target: previewContent
|
||||
onHoveredChanged: {
|
||||
root.hoveredLink = hovered ? LinkPreviewInfo.url : "";
|
||||
rootDelegate.hoveredLink = hovered ? LinkPreviewInfo.url : "";
|
||||
}
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
@ -204,7 +218,7 @@ SBSMessageBase {
|
||||
wrapMode: Label.WrapAtWordBoundaryOrAnywhere
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: TextEdit.RichText
|
||||
color: root.colorText
|
||||
color: rootDelegate.colorText
|
||||
visible: LinkPreviewInfo.title.length > 0
|
||||
text: LinkPreviewInfo.title
|
||||
lineHeight: 1.3
|
||||
@ -217,9 +231,9 @@ SBSMessageBase {
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: TextEdit.RichText
|
||||
visible: LinkPreviewInfo.description.length > 0
|
||||
font.underline: root.hoveredLink
|
||||
font.underline: rootDelegate.hoveredLink
|
||||
text: LinkPreviewInfo.description
|
||||
color: root.colorUrl
|
||||
color: rootDelegate.colorUrl
|
||||
lineHeight: 1.3
|
||||
}
|
||||
Label {
|
||||
@ -229,7 +243,7 @@ SBSMessageBase {
|
||||
wrapMode: Label.WrapAtWordBoundaryOrAnywhere
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: TextEdit.RichText
|
||||
color: root.colorText
|
||||
color: rootDelegate.colorText
|
||||
text: LinkPreviewInfo.domain
|
||||
lineHeight: 1.3
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ MenuItem {
|
||||
id: menuItem
|
||||
|
||||
property string itemName: ""
|
||||
property bool bold: false
|
||||
property string content: ""
|
||||
property alias iconSource: contextMenuItemImage.source
|
||||
property string iconColor: ""
|
||||
@ -99,6 +100,7 @@ MenuItem {
|
||||
anchors.left: parent.left
|
||||
height: parent.height
|
||||
text: itemName
|
||||
font.bold: bold
|
||||
color: dangerous ? JamiTheme.redColor : isActif ? JamiTheme.textColor : JamiTheme.chatViewFooterImgColor
|
||||
font.pointSize: JamiTheme.textFontSize
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "version.h"
|
||||
#include "version_info.h"
|
||||
|
||||
#include <QVariantMap>
|
||||
@ -108,7 +107,7 @@ protected:
|
||||
{"platform", QSysInfo::prettyProductName() + "_" + QSysInfo::currentCpuArchitecture()},
|
||||
{"client_sha", APP_VERSION_STRING},
|
||||
{"jamicore_sha", CORE_VERSION_STRING},
|
||||
{"build_id", QString(VERSION_STRING)},
|
||||
{"build_id", BUILD_VERSION_STRING},
|
||||
#if defined(Q_OS_WIN) && defined(BETA)
|
||||
{"build_variant", "beta"},
|
||||
#endif
|
||||
|
||||
@ -147,6 +147,7 @@ CurrentAccount::updateData()
|
||||
set_isRendezVous(accConfig.isRendezVous, true);
|
||||
set_dhtPort(accConfig.dhtPort, true);
|
||||
set_autoAnswer(accConfig.autoAnswer, true);
|
||||
set_denySecondCall(accConfig.denySecondCall, true);
|
||||
set_proxyEnabled(accConfig.proxyEnabled, true);
|
||||
set_upnpEnabled(accConfig.upnpEnabled, true);
|
||||
set_publishedSameAsLocal(accConfig.publishedSameAsLocal, true);
|
||||
|
||||
@ -121,6 +121,7 @@ class CurrentAccount final : public QObject
|
||||
QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, sendComposing)
|
||||
QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, isRendezVous)
|
||||
QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, autoAnswer)
|
||||
QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, denySecondCall)
|
||||
QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, proxyEnabled)
|
||||
QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, upnpEnabled)
|
||||
QML_ACCOUNT_CONFIG_SETTINGS_PROPERTY(bool, publishedSameAsLocal)
|
||||
|
||||
@ -356,7 +356,7 @@ CurrentCall::onCurrentAccountIdChanged()
|
||||
}
|
||||
|
||||
void
|
||||
CurrentCall::onCallStatusChanged(const QString& callId, int code)
|
||||
CurrentCall::onCallStatusChanged(const QString& accountId, const QString& callId, int code)
|
||||
{
|
||||
Q_UNUSED(code)
|
||||
|
||||
|
||||
@ -81,7 +81,7 @@ private:
|
||||
private Q_SLOTS:
|
||||
void onCurrentConvIdChanged();
|
||||
void onCurrentAccountIdChanged();
|
||||
void onCallStatusChanged(const QString& callId, int code);
|
||||
void onCallStatusChanged(const QString& accountId, const QString& callId, int code);
|
||||
void onCallInfosChanged(const QString& accountId, const QString& callId);
|
||||
void onCurrentCallChanged(const QString& callId);
|
||||
void onParticipantsChanged(const QString& callId);
|
||||
|
||||
@ -371,7 +371,7 @@ CurrentConversation::updateActiveCalls(const QString&, const QString& convId)
|
||||
}
|
||||
|
||||
void
|
||||
CurrentConversation::onCallStatusChanged(const QString& callId, int)
|
||||
CurrentConversation::onCallStatusChanged(const QString& accountId, const QString& callId, int)
|
||||
{
|
||||
if (callId != callId_) {
|
||||
return;
|
||||
|
||||
@ -90,7 +90,7 @@ private Q_SLOTS:
|
||||
void updateErrors(const QString& convId);
|
||||
void updateConversationPreferences(const QString& convId);
|
||||
void updateActiveCalls(const QString&, const QString& convId);
|
||||
void onCallStatusChanged(const QString& callId, int code);
|
||||
void onCallStatusChanged(const QString& accountId, const QString& callId, int code);
|
||||
void onShowIncomingCallView(const QString& accountId, const QString& convUid);
|
||||
|
||||
Q_SIGNALS:
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
#include "mainapplication.h"
|
||||
#include "instancemanager.h"
|
||||
#include "version.h"
|
||||
#include "version_info.h"
|
||||
#if defined(Q_OS_MACOS)
|
||||
#include <os/macos/macutils.h>
|
||||
#endif
|
||||
@ -66,7 +66,7 @@ main(int argc, char* argv[])
|
||||
QApplication::setApplicationName(QStringLiteral("Jami"));
|
||||
QApplication::setOrganizationDomain(QStringLiteral("jami.net"));
|
||||
QApplication::setQuitOnLastWindowClosed(false);
|
||||
QCoreApplication::setApplicationVersion(QString(VERSION_STRING));
|
||||
QCoreApplication::setApplicationVersion(BUILD_VERSION_STRING);
|
||||
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts, true);
|
||||
QApplication::setHighDpiScaleFactorRoundingPolicy(
|
||||
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||
|
||||
@ -20,7 +20,6 @@
|
||||
#include "global.h"
|
||||
#include "qmlregister.h"
|
||||
#include "appsettingsmanager.h"
|
||||
#include "spellcheckdictionarymanager.h"
|
||||
#include "connectivitymonitor.h"
|
||||
#include "systemtray.h"
|
||||
#include "previewengine.h"
|
||||
@ -160,6 +159,7 @@ MainApplication::MainApplication(int& argc, char** argv)
|
||||
"qml.debug=false\n"
|
||||
"default.debug=false\n"
|
||||
"client.debug=false\n"
|
||||
"spellcheck.debug=false\n"
|
||||
"\n");
|
||||
// These can be set in the environment as well.
|
||||
// e.g. QT_LOGGING_RULES="*.debug=false;qml.debug=true"
|
||||
@ -191,7 +191,6 @@ MainApplication::init()
|
||||
// to any other initialization. This won't do anything if crashpad isn't
|
||||
// enabled.
|
||||
settingsManager_ = new AppSettingsManager(this);
|
||||
spellCheckDictionaryManager_ = new SpellCheckDictionaryManager(settingsManager_, this);
|
||||
crashReporter_ = new CrashReporter(settingsManager_, this);
|
||||
|
||||
// This 2-phase initialisation prevents ephemeral instances from
|
||||
@ -349,8 +348,8 @@ MainApplication::parseArguments()
|
||||
parser_.addOption(muteDaemonOption);
|
||||
|
||||
#ifdef QT_DEBUG
|
||||
// In debug mode, add an option to test a specific QML component via its name.
|
||||
// e.g. ./jami --test AccountComboBox
|
||||
// In debug mode, add an option to test a specific QML component via its name.
|
||||
// e.g. ./jami --test AccountComboBox
|
||||
parser_.addOption(QCommandLineOption("test", "Test a QML component via its name.", "uri"));
|
||||
// We may need to force the test window dimensions in the case that the component to test
|
||||
// does not specify its own dimensions and is dependent on parent/sibling dimensions.
|
||||
@ -425,7 +424,6 @@ MainApplication::initQmlLayer()
|
||||
lrcInstance_.get(),
|
||||
systemTray_,
|
||||
settingsManager_,
|
||||
spellCheckDictionaryManager_,
|
||||
connectivityMonitor_,
|
||||
previewEngine_,
|
||||
&screenInfo_,
|
||||
|
||||
@ -31,7 +31,6 @@
|
||||
class ConnectivityMonitor;
|
||||
class SystemTray;
|
||||
class AppSettingsManager;
|
||||
class SpellCheckDictionaryManager;
|
||||
class CrashReporter;
|
||||
class PreviewEngine;
|
||||
|
||||
@ -119,7 +118,6 @@ private:
|
||||
ConnectivityMonitor* connectivityMonitor_;
|
||||
SystemTray* systemTray_;
|
||||
AppSettingsManager* settingsManager_;
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager_;
|
||||
PreviewEngine* previewEngine_;
|
||||
CrashReporter* crashReporter_;
|
||||
|
||||
|
||||
@ -85,7 +85,6 @@ Popup {
|
||||
anchors.rightMargin: 15
|
||||
spacing: 10
|
||||
|
||||
|
||||
Avatar {
|
||||
id: avatar
|
||||
objectName: "accountComboBoxPopupAvatar"
|
||||
@ -164,8 +163,13 @@ Popup {
|
||||
imageColor: hovered ? JamiTheme.textColor : JamiTheme.buttonTintedGreyHovered
|
||||
hoveredColor: JamiTheme.hoverColor
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: toolTipText
|
||||
Accessible.description: JamiStrings.qrCodeExplanation
|
||||
|
||||
onClicked: {
|
||||
viewCoordinator.presentDialog(appWindow, "mainview/components/WelcomePageQrDialog.qml");
|
||||
listView.currentIndex = -1;
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
@ -184,34 +188,40 @@ Popup {
|
||||
|
||||
toolTipText: !inSettings ? JamiStrings.openSettings : JamiStrings.closeSettings
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: toolTipText
|
||||
KeyNavigation.backtab: shareButton
|
||||
|
||||
onClicked: {
|
||||
!inSettings ? viewCoordinator.present("SettingsView") : viewCoordinator.dismiss("SettingsView");
|
||||
root.close();
|
||||
}
|
||||
|
||||
KeyNavigation.tab: addAccountItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle{
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
height: 1
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 15
|
||||
Layout.rightMargin: 15
|
||||
color: JamiTheme.smartListHoveredColor
|
||||
}
|
||||
|
||||
|
||||
JamiListView {
|
||||
ListView {
|
||||
id: listView
|
||||
objectName: "accountList"
|
||||
Accessible.name: JamiStrings.accountList
|
||||
Accessible.role: Accessible.List
|
||||
Accessible.description: JamiStrings.accountListDescription
|
||||
|
||||
layer.mipmap: false
|
||||
clip: true
|
||||
maximumFlickVelocity: 1024
|
||||
|
||||
// HACK: remove after migration to Qt 6.7+
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: parent.width
|
||||
|
||||
activeFocusOnTab: true
|
||||
focus: true
|
||||
currentIndex: -1 // Set to -1 to avoid initial highlighting
|
||||
|
||||
model: SortFilterProxyModel {
|
||||
sourceModel: AccountListModel
|
||||
filters: ValueFilter {
|
||||
@ -221,10 +231,35 @@ Popup {
|
||||
}
|
||||
}
|
||||
|
||||
highlight: Rectangle {
|
||||
color: "transparent"
|
||||
border.color: JamiTheme.primaryBackgroundColor
|
||||
border.width: 2
|
||||
radius: 5
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: JamiTheme.hoverColor
|
||||
radius: 5
|
||||
opacity: 0.3
|
||||
}
|
||||
}
|
||||
|
||||
delegate: AccountItemDelegate {
|
||||
height: JamiTheme.accountListItemHeight
|
||||
width: root.width
|
||||
|
||||
Accessible.role: Accessible.ListItem
|
||||
Accessible.name: Alias || Username
|
||||
Accessible.description: JamiStrings.switchToAccount
|
||||
|
||||
// Update the background to show focus state
|
||||
background: Rectangle {
|
||||
color: parent.activeFocus || parent.hovered ? JamiTheme.hoverColor : "transparent"
|
||||
opacity: parent.activeFocus ? 0.3 : 1
|
||||
radius: 5
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
root.close();
|
||||
// This is a workaround for the synchronicity issue
|
||||
@ -235,7 +270,7 @@ Popup {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle{
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
height: 1
|
||||
Layout.fillWidth: true
|
||||
@ -248,19 +283,24 @@ Popup {
|
||||
id: addAccountItem
|
||||
|
||||
Layout.preferredHeight: 45
|
||||
Layout.preferredWidth: parent.width -10
|
||||
Layout.preferredWidth: parent.width - 10
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.leftMargin: 5
|
||||
|
||||
Accessible.name: JamiStrings.addAccount
|
||||
focusPolicy: Qt.StrongFocus
|
||||
Accessible.name: addAccountText.text
|
||||
Accessible.role: Accessible.Button
|
||||
|
||||
KeyNavigation.tab: manageAccountItem
|
||||
KeyNavigation.up: listView
|
||||
KeyNavigation.down: manageAccountItem
|
||||
|
||||
background: Rectangle {
|
||||
color: addAccountItem.hovered ? JamiTheme.hoverColor : JamiTheme.accountComboBoxBackgroundColor
|
||||
radius: 5
|
||||
}
|
||||
|
||||
RowLayout{
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@ -274,6 +314,7 @@ Popup {
|
||||
}
|
||||
|
||||
Text {
|
||||
id: addAccountText
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: JamiStrings.addAccount
|
||||
textFormat: TextEdit.PlainText
|
||||
@ -285,18 +326,21 @@ Popup {
|
||||
root.close();
|
||||
viewCoordinator.present("WizardView");
|
||||
}
|
||||
|
||||
KeyNavigation.tab: manageAccountItem
|
||||
}
|
||||
|
||||
ItemDelegate {
|
||||
id: manageAccountItem
|
||||
|
||||
focusPolicy: Qt.StrongFocus
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: JamiStrings.manageAccount
|
||||
Accessible.name: manageAccountText.text
|
||||
|
||||
KeyNavigation.backtab: addAccountItem
|
||||
KeyNavigation.tab: shareButton
|
||||
KeyNavigation.up: addAccountItem
|
||||
|
||||
Layout.preferredHeight: 45
|
||||
Layout.preferredWidth: parent.width-10
|
||||
Layout.preferredWidth: parent.width - 10
|
||||
Layout.leftMargin: 5
|
||||
Layout.bottomMargin: 5
|
||||
|
||||
@ -305,7 +349,7 @@ Popup {
|
||||
radius: 5
|
||||
}
|
||||
|
||||
RowLayout{
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@ -319,8 +363,8 @@ Popup {
|
||||
color: manageAccountItem.hovered ? JamiTheme.textColor : JamiTheme.buttonTintedGreyHovered
|
||||
}
|
||||
Text {
|
||||
id: manageAccountText
|
||||
text: JamiStrings.manageAccount
|
||||
|
||||
textFormat: TextEdit.PlainText
|
||||
color: JamiTheme.textColor
|
||||
font.pointSize: JamiTheme.textFontSize
|
||||
@ -328,7 +372,7 @@ Popup {
|
||||
}
|
||||
onClicked: {
|
||||
root.close();
|
||||
viewCoordinator.present("SettingsView")
|
||||
viewCoordinator.present("SettingsView");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,17 +28,24 @@ ItemDelegate {
|
||||
height: JamiTheme.accountListItemHeight
|
||||
|
||||
background: Rectangle {
|
||||
width: root.width - 10
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors {
|
||||
left: parent ? parent.left : undefined
|
||||
right: parent ? parent.right : undefined
|
||||
leftMargin: 5
|
||||
rightMargin: 5
|
||||
}
|
||||
radius: 5
|
||||
|
||||
Rectangle{
|
||||
Rectangle {
|
||||
id: separationLine
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
leftMargin: 10
|
||||
rightMargin: 10
|
||||
}
|
||||
height: 1
|
||||
width: parent.width - 20
|
||||
color: JamiTheme.hoverColor
|
||||
visible: index !== 0
|
||||
}
|
||||
|
||||
@ -25,6 +25,13 @@ import "../../commoncomponents"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string tipTitle: title.text
|
||||
property string tipDescription: opened ? description.text : JamiStrings.whyBackupAccount
|
||||
|
||||
Accessible.name: tipTitle
|
||||
Accessible.description: tipDescription
|
||||
|
||||
width: parent.width
|
||||
height: backupLayout.height
|
||||
|
||||
|
||||
@ -55,15 +55,30 @@ Control {
|
||||
signal fullScreenClicked
|
||||
signal swarmDetailsClicked
|
||||
|
||||
// For Keyboard naviguation
|
||||
property bool isInternalNavigation: false
|
||||
|
||||
function exitBarNavigation() {
|
||||
isInternalNavigation = false;
|
||||
// Let the parent control take over focus handling
|
||||
parent.forceActiveFocus();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: buttonDelegate
|
||||
|
||||
CallButtonDelegate {
|
||||
id: delegateItem
|
||||
width: root.height
|
||||
height: width
|
||||
barWidth: root.width
|
||||
onSubMenuVisibleChanged: subMenuOpen = subMenuVisible
|
||||
onHoveredChanged: root.barHovered = hovered
|
||||
|
||||
focusPolicy: Qt.StrongFocus
|
||||
focus: false
|
||||
|
||||
Keys.onEscapePressed: root.exitBarNavigation()
|
||||
}
|
||||
}
|
||||
|
||||
@ -567,6 +582,34 @@ Control {
|
||||
ComboBox {
|
||||
id: overflowButton
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: JamiStrings.more
|
||||
Accessible.description: JamiStrings.moreOptions
|
||||
|
||||
KeyNavigation.tab: {
|
||||
if (popup.opened) {
|
||||
return popup.contentItem.itemAtIndex(0);
|
||||
}
|
||||
// Exit bar navigation if we've reached the end
|
||||
root.exitBarNavigation();
|
||||
return null;
|
||||
}
|
||||
|
||||
KeyNavigation.backtab: {
|
||||
if (overflowItemListView.count > 0) {
|
||||
return overflowItemListView.itemAtIndex(overflowItemListView.count - 1);
|
||||
}
|
||||
return itemListView.itemAtIndex(itemListView.count - 1);
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
if (popup.opened) {
|
||||
popup.close();
|
||||
} else {
|
||||
root.exitBarNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
visible: CallOverlayModel.overflowIndex < overflowItemCount - 2
|
||||
width: root.height
|
||||
height: width
|
||||
|
||||
@ -42,8 +42,18 @@ ItemDelegate {
|
||||
text: ""
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: text
|
||||
Accessible.description: text
|
||||
Accessible.name: ItemAction.text
|
||||
Accessible.description: {
|
||||
if (!ItemAction?.text)
|
||||
return "";
|
||||
if (ItemAction.checkable) {
|
||||
return JamiStrings.pressToToggle.arg(ItemAction.text).arg(ItemAction.checked ? JamiStrings.active : JamiStrings.inactive);
|
||||
}
|
||||
return JamiStrings.pressToAction.arg(ItemAction.text);
|
||||
}
|
||||
Accessible.pressed: pressed
|
||||
Accessible.checkable: ItemAction ? ItemAction.checkable : false
|
||||
Accessible.checked: ItemAction ? ItemAction.checked : false
|
||||
|
||||
z: index
|
||||
|
||||
@ -77,7 +87,7 @@ ItemDelegate {
|
||||
return HalfPill.None;
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: JamiTheme.shortFadeDuration
|
||||
}
|
||||
@ -94,7 +104,7 @@ ItemDelegate {
|
||||
radius: isLast ? 5 : width / 2
|
||||
type: isLast ? HalfPill.Right : HalfPill.None
|
||||
|
||||
Behavior on color {
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: JamiTheme.shortFadeDuration
|
||||
}
|
||||
@ -113,7 +123,7 @@ ItemDelegate {
|
||||
source: ItemAction ? ItemAction.icon.source : ""
|
||||
color: ItemAction ? (ItemAction.enabled ? ItemAction.icon.color : Qt.lighter(ItemAction.icon.color)) : null
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
SequentialAnimation on opacity {
|
||||
loops: Animation.Infinite
|
||||
running: ItemAction !== undefined && ItemAction.blinksWhenChecked !== undefined && ItemAction.blinksWhenChecked && checked
|
||||
onStopped: icon.opacity = 1
|
||||
@ -188,7 +198,7 @@ ItemDelegate {
|
||||
radius: 4
|
||||
}
|
||||
|
||||
onActivated: index => menuAction.accept(index);
|
||||
onActivated: index => menuAction.accept(index)
|
||||
model: visible ? menuAction.listModel : null
|
||||
delegate: ItemDelegate {
|
||||
id: menuItem
|
||||
@ -313,7 +323,6 @@ ItemDelegate {
|
||||
// it fits within the overlay, with an extra leftward margin of 24 pixels.
|
||||
return diff > 0 ? xValue - diff - 24 : xValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
implicitWidth: contentItem.implicitWidth
|
||||
|
||||
@ -26,6 +26,12 @@ ColumnLayout {
|
||||
id: column
|
||||
width: parent.width
|
||||
|
||||
property string tipTitle: JamiStrings.customize
|
||||
property string tipDescription: JamiStrings.customizeText
|
||||
|
||||
Accessible.name: tipTitle
|
||||
Accessible.description: tipDescription
|
||||
|
||||
property var iconSize: 26
|
||||
property var margin: 5
|
||||
property var prefWidth: 170
|
||||
|
||||
@ -24,6 +24,13 @@ import "../../commoncomponents"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string tipTitle: title.text
|
||||
property string tipDescription: content.text
|
||||
|
||||
Accessible.name: tipTitle
|
||||
Accessible.description: tipDescription
|
||||
|
||||
focus: true
|
||||
width: parent.width
|
||||
height: backupLayout.height
|
||||
|
||||
@ -43,6 +43,9 @@ TabButton {
|
||||
hoverEnabled: true
|
||||
onClicked: selected()
|
||||
|
||||
Accessible.name: root.labelText
|
||||
Accessible.role: Accessible.Button
|
||||
|
||||
Rectangle {
|
||||
id: contentRect
|
||||
|
||||
|
||||
@ -26,6 +26,12 @@ ColumnLayout {
|
||||
id: column
|
||||
width: parent.width
|
||||
|
||||
property alias tipTitle: title.text
|
||||
property alias tipDescription: description.text
|
||||
|
||||
Accessible.name: tipTitle
|
||||
Accessible.description: tipDescription
|
||||
|
||||
property real maxHeight: 250
|
||||
|
||||
property var iconSize: 26
|
||||
|
||||
@ -25,4 +25,6 @@ PushButton {
|
||||
|
||||
normalColor: JamiTheme.chatviewBgColor
|
||||
imageColor: hovered ? JamiTheme.chatviewButtonColor : JamiTheme.chatViewFooterImgColor
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.description: toolTipText
|
||||
}
|
||||
|
||||
@ -228,6 +228,11 @@ Window {
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
background: Rectangle {
|
||||
anchors.fill: parent
|
||||
color: JamiTheme.backgroundColor
|
||||
}
|
||||
|
||||
// make a list view of keyboardShortcutsModelList[selectionBar.currentIndex]
|
||||
JamiListView {
|
||||
id: keyboardShortcutsListView
|
||||
@ -262,6 +267,7 @@ Window {
|
||||
Layout.topMargin: 8
|
||||
Layout.leftMargin: 20
|
||||
text: description
|
||||
color: JamiTheme.textColor
|
||||
background: Rectangle {
|
||||
width: parent.width + 16
|
||||
height: parent.height + 16
|
||||
@ -269,6 +275,7 @@ Window {
|
||||
border.width: 2
|
||||
radius: 5
|
||||
anchors.centerIn: parent
|
||||
color: JamiTheme.backgroundColor
|
||||
}
|
||||
}
|
||||
Label {
|
||||
@ -277,6 +284,7 @@ Window {
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 20
|
||||
text: shortcut
|
||||
color: JamiTheme.textColor
|
||||
background: Rectangle {
|
||||
width: parent.width + 16
|
||||
height: parent.height + 16
|
||||
@ -284,6 +292,7 @@ Window {
|
||||
border.width: 2
|
||||
radius: 5
|
||||
anchors.centerIn: parent
|
||||
color: JamiTheme.backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -300,6 +309,10 @@ Window {
|
||||
|
||||
focus: true
|
||||
|
||||
background: Rectangle {
|
||||
color: JamiTheme.backgroundColor
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: [JamiStrings.generalSettingsTitle, JamiStrings.conversationKeyboardShortcuts, JamiStrings.callKeyboardShortcuts, JamiStrings.markdownKeyboardShortcuts, JamiStrings.settings]
|
||||
|
||||
@ -339,9 +352,16 @@ Window {
|
||||
footer: Item {
|
||||
height: JamiTheme.keyboardShortcutTabBarSize
|
||||
PageIndicator {
|
||||
id: pageIndicator
|
||||
anchors.centerIn: parent
|
||||
count: selectionBar.count
|
||||
currentIndex: selectionBar.currentIndex
|
||||
delegate: Rectangle {
|
||||
width: 6
|
||||
height: 6
|
||||
radius: 3
|
||||
color: index === pageIndicator.currentIndex ? JamiTheme.textColor : JamiTheme.textColorHoveredHighContrast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,17 @@ Item {
|
||||
|
||||
property string timeText: "00:00"
|
||||
property string remoteRecordingLabel
|
||||
property bool isKeyboardSelectionActive: {
|
||||
if (!appWindow || !appWindow.activeFocusItem)
|
||||
return false;
|
||||
let parent = appWindow.activeFocusItem.parent;
|
||||
while (parent && parent !== appWindow && parent !== root && parent !== null) {
|
||||
if (parent.objectName === "callActionBar")
|
||||
return true;
|
||||
parent = parent.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CurrentCall
|
||||
@ -42,7 +53,11 @@ Item {
|
||||
|
||||
property alias callActionBar: __callActionBar
|
||||
|
||||
property bool frozen: callActionBar.overflowOpen || callActionBar.barHovered || callActionBar.subMenuOpen || participantCallInStatusView.visible
|
||||
property bool frozen: callActionBar.overflowOpen ||
|
||||
callActionBar.barHovered ||
|
||||
callActionBar.subMenuOpen ||
|
||||
participantCallInStatusView.visible ||
|
||||
isKeyboardSelectionActive
|
||||
|
||||
property string muteAlertMessage: ""
|
||||
property bool muteAlertActive: false
|
||||
@ -59,15 +74,27 @@ Item {
|
||||
Component.onDestruction: CallOverlayModel.setEventFilterActive(appWindow, this, false)
|
||||
onVisibleChanged: CallOverlayModel.setEventFilterActive(appWindow, this, visible)
|
||||
|
||||
function kickOverlay() {
|
||||
root.opacity = 1;
|
||||
fadeOutTimer.restart();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CallOverlayModel
|
||||
|
||||
function onMouseMoved(item) {
|
||||
if (item === root) {
|
||||
root.opacity = 1;
|
||||
fadeOutTimer.restart();
|
||||
kickOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// This is part of a mechanism used to show the overlay when a focus key is pressed
|
||||
// and keep it open in the case that the user is navigating with the keyboard over
|
||||
// the call action bar.
|
||||
function onFocusKeyPressed() {
|
||||
// Always show the overlay when a focus key (Tab/BackTab) is pressed
|
||||
kickOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
@ -76,8 +103,7 @@ Item {
|
||||
context: Qt.ApplicationShortcut
|
||||
onActivated: {
|
||||
CallAdapter.muteAudioToggle();
|
||||
root.opacity = 1;
|
||||
fadeOutTimer.restart();
|
||||
kickOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,8 +113,7 @@ Item {
|
||||
context: Qt.ApplicationShortcut
|
||||
onActivated: {
|
||||
CallAdapter.muteCameraToggle();
|
||||
root.opacity = 1;
|
||||
fadeOutTimer.restart();
|
||||
kickOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -37,7 +37,6 @@ JamiFlickable {
|
||||
property bool showPreview: false
|
||||
property bool isShowTypo: UtilsAdapter.getAppValue(Settings.Key.ShowMardownOption)
|
||||
property int textWidth: textArea.contentWidth
|
||||
property var spellCheckActive: AppSettingsManager.getValue(Settings.EnableSpellCheck)
|
||||
property var language: AppSettingsManager.getValue(Settings.SpellLang)
|
||||
|
||||
// Used to cache the editable text when showing the preview message
|
||||
@ -73,7 +72,7 @@ JamiFlickable {
|
||||
|
||||
lineEditObj: textArea
|
||||
customizePaste: true
|
||||
checkSpell: (Qt.platform.os.toString() === "linux") ? true : false
|
||||
spellCheckEnabled: root.spellCheckEnabled
|
||||
|
||||
onContextMenuRequirePaste: {
|
||||
// Intercept paste event to use C++ QMimeData
|
||||
@ -114,73 +113,41 @@ JamiFlickable {
|
||||
}
|
||||
}
|
||||
|
||||
property bool spellCheckEnabled: AppSettingsManager.getValue(Settings.EnableSpellCheck) && AppSettingsManager.getValue(Settings.SpellLang) !== ""
|
||||
|
||||
// Spell check is active under the following conditions:
|
||||
// 1. Spell check is enabled in settings
|
||||
// 2. The selected spell language is not ""
|
||||
// 3. We are not in preview mode
|
||||
readonly property bool spellCheckActive: spellCheckEnabled && !showPreview
|
||||
|
||||
onSpellCheckActiveChanged: textArea.updateSpellCorrection()
|
||||
|
||||
TextArea.flickable: TextArea {
|
||||
id: textArea
|
||||
|
||||
CachedFile {
|
||||
id: cachedFile
|
||||
Connections {
|
||||
target: SpellCheckAdapter
|
||||
|
||||
function onDictionaryChanged() {
|
||||
textArea.updateSpellCorrection();
|
||||
}
|
||||
}
|
||||
|
||||
function updateCorrection(language) {
|
||||
cachedFile.updateDictionnary(language);
|
||||
textArea.updateUnderlineText();
|
||||
}
|
||||
|
||||
// Listen to settings changes and apply it to this widget
|
||||
// Listen to settings changes to apply it to the text area
|
||||
Connections {
|
||||
target: UtilsAdapter
|
||||
|
||||
function onChangeLanguage() {
|
||||
textArea.updateUnderlineText();
|
||||
textArea.updateSpellCorrection();
|
||||
}
|
||||
|
||||
function onChangeFontSize() {
|
||||
textArea.updateUnderlineText();
|
||||
}
|
||||
|
||||
function onSpellLanguageChanged() {
|
||||
root.language = SpellCheckDictionaryManager.getSpellLanguage();
|
||||
if ((Qt.platform.os.toString() !== "linux") || (AppSettingsManager.getValue(Settings.SpellLang) === "NONE")) {
|
||||
spellCheckActive = false;
|
||||
} else {
|
||||
spellCheckActive = AppSettingsManager.getValue(Settings.EnableSpellCheck);
|
||||
}
|
||||
if (spellCheckActive === true) {
|
||||
root.language = SpellCheckDictionaryManager.getSpellLanguage();
|
||||
textArea.updateCorrection(root.language);
|
||||
} else {
|
||||
textArea.clearUnderlines();
|
||||
}
|
||||
textArea.updateSpellCorrection();
|
||||
}
|
||||
|
||||
function onEnableSpellCheckChanged() {
|
||||
// Disable spell check on non-linux platforms yet
|
||||
if ((Qt.platform.os.toString() !== "linux") || (AppSettingsManager.getValue(Settings.SpellLang) === "NONE")) {
|
||||
spellCheckActive = false;
|
||||
} else {
|
||||
spellCheckActive = AppSettingsManager.getValue(Settings.EnableSpellCheck);
|
||||
}
|
||||
if (spellCheckActive === true) {
|
||||
root.language = SpellCheckDictionaryManager.getSpellLanguage();
|
||||
textArea.updateCorrection(root.language);
|
||||
} else {
|
||||
textArea.clearUnderlines();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the settings if the component wasn't loaded when changing settings
|
||||
Component.onCompleted: {
|
||||
if ((Qt.platform.os.toString() !== "linux") || (AppSettingsManager.getValue(Settings.SpellLang) === "NONE")) {
|
||||
spellCheckActive = false;
|
||||
} else {
|
||||
spellCheckActive = AppSettingsManager.getValue(Settings.EnableSpellCheck);
|
||||
}
|
||||
if (spellCheckActive === true) {
|
||||
root.language = SpellCheckDictionaryManager.getSpellLanguage();
|
||||
textArea.updateCorrection(root.language);
|
||||
} else {
|
||||
textArea.clearUnderlines();
|
||||
textArea.updateSpellCorrection();
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,13 +190,15 @@ JamiFlickable {
|
||||
|
||||
onReleased: function (event) {
|
||||
if (event.button === Qt.RightButton) {
|
||||
var position = textArea.positionAt(event.x, event.y);
|
||||
textArea.moveCursorSelection(position, TextInput.SelectWords);
|
||||
textArea.selectWord();
|
||||
if (!MessagesAdapter.spell(textArea.selectedText)) {
|
||||
var wordList = MessagesAdapter.spellSuggestionsRequest(textArea.selectedText);
|
||||
if (wordList.length !== 0) {
|
||||
textAreaContextMenu.addMenuItem(wordList);
|
||||
if (spellCheckActive && SpellCheckAdapter.hasLoadedDictionary) {
|
||||
var position = textArea.positionAt(event.x, event.y);
|
||||
textArea.moveCursorSelection(position, TextInput.SelectWords);
|
||||
textArea.selectWord();
|
||||
if (!SpellCheckAdapter.spell(textArea.selectedText)) {
|
||||
var wordList = SpellCheckAdapter.spellSuggestionsRequest(textArea.selectedText);
|
||||
if (wordList.length !== 0) {
|
||||
textAreaContextMenu.addMenuItem(wordList);
|
||||
}
|
||||
}
|
||||
}
|
||||
textAreaContextMenu.openMenuAt(event);
|
||||
@ -237,7 +206,7 @@ JamiFlickable {
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
updateUnderlineText();
|
||||
updateSpellCorrection();
|
||||
if (text !== debounceText && !showPreview) {
|
||||
debounceText = text;
|
||||
MessagesAdapter.userIsComposing(text ? true : false);
|
||||
@ -250,7 +219,7 @@ JamiFlickable {
|
||||
// Shift + Enter -> Next Line
|
||||
Keys.onPressed: function (keyEvent) {
|
||||
// Update underline on each input to take into account deleted text and sent ones
|
||||
updateUnderlineText();
|
||||
updateSpellCorrection();
|
||||
if (keyEvent.matches(StandardKey.Paste)) {
|
||||
MessagesAdapter.onPaste();
|
||||
keyEvent.accepted = true;
|
||||
@ -280,17 +249,17 @@ JamiFlickable {
|
||||
}
|
||||
}
|
||||
|
||||
function updateUnderlineText() {
|
||||
function updateSpellCorrection() {
|
||||
clearUnderlines();
|
||||
// We iterate over the whole text to find words to check and underline them if needed
|
||||
if (spellCheckActive) {
|
||||
if (spellCheckActive && SpellCheckAdapter.hasLoadedDictionary) {
|
||||
var text = textArea.text;
|
||||
var words = MessagesAdapter.findWords(text);
|
||||
var words = SpellCheckAdapter.findWords(text);
|
||||
if (!words)
|
||||
return;
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
var wordInfo = words[i];
|
||||
if (wordInfo && wordInfo.word && !MessagesAdapter.spell(wordInfo.word)) {
|
||||
if (wordInfo && wordInfo.word && !SpellCheckAdapter.spell(wordInfo.word)) {
|
||||
textMetrics.text = wordInfo.word;
|
||||
var xPos = textArea.positionToRectangle(wordInfo.position).x;
|
||||
var yPos = textArea.positionToRectangle(wordInfo.position).y + textArea.positionToRectangle(wordInfo.position).height;
|
||||
|
||||
@ -479,6 +479,9 @@ Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
enabled: !showPreview
|
||||
hoverEnabled: !showPreview
|
||||
Accessible.name: JamiStrings.showMoreMessagingOptions
|
||||
Accessible.role: Accessible.ComboBox
|
||||
Accessible.description: JamiStrings.showMoreMessagingOptionsDescription
|
||||
|
||||
// Used to choose the correct color for the button.
|
||||
readonly property bool highlight: down || hovered
|
||||
|
||||
@ -24,8 +24,26 @@ import net.jami.Adapters 1.1
|
||||
import net.jami.Constants 1.1
|
||||
import "../../commoncomponents"
|
||||
|
||||
JamiListView {
|
||||
ListView {
|
||||
id: root
|
||||
property alias verticalScrollBar: verticalScrollBar
|
||||
layer.mipmap: false
|
||||
clip: true
|
||||
|
||||
ScrollBar.vertical: JamiScrollBar {
|
||||
id: verticalScrollBar
|
||||
|
||||
attachedFlickableMoving: root.moving
|
||||
}
|
||||
|
||||
keyNavigationEnabled: true
|
||||
keyNavigationWraps: false
|
||||
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
|
||||
Accessible.role: Accessible.List
|
||||
Accessible.name: JamiStrings.conversationMessages
|
||||
|
||||
function getDistanceToBottom() {
|
||||
const scrollDiff = ScrollBar.vertical.position - (1.0 - ScrollBar.vertical.size);
|
||||
@ -139,7 +157,10 @@ JamiListView {
|
||||
}
|
||||
|
||||
// fade-in mechanism
|
||||
Component.onCompleted: fadeAnimation.start()
|
||||
Component.onCompleted: {
|
||||
positionViewAtBeginning();
|
||||
fadeAnimation.start();
|
||||
}
|
||||
Rectangle {
|
||||
id: overlay
|
||||
anchors.fill: parent
|
||||
@ -194,13 +215,19 @@ JamiListView {
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
currentIndex: -1
|
||||
|
||||
Connections {
|
||||
target: CurrentConversation
|
||||
function onIdChanged() {
|
||||
currentIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
model: MessagesAdapter.messageListModel
|
||||
delegate: DelegateChooser {
|
||||
id: delegateChooser
|
||||
role: "Type"
|
||||
|
||||
DelegateChoice {
|
||||
id: delegateChoice
|
||||
roleValue: Interaction.Type.TEXT
|
||||
|
||||
TextMessageDelegate {
|
||||
|
||||
@ -220,7 +220,6 @@ Popup {
|
||||
id: cancelBtn
|
||||
objectName: "cancelBtn"
|
||||
z: 1
|
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
Layout.preferredHeight: 20
|
||||
Layout.preferredWidth: 20
|
||||
@ -323,6 +322,9 @@ Popup {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
preferredSize: btnSize
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: toolTipText
|
||||
|
||||
source: JamiResources.stop_rectangle_24dp_svg
|
||||
|
||||
imageColor: JamiTheme.whiteColor
|
||||
|
||||
@ -34,7 +34,13 @@ Rectangle {
|
||||
|
||||
function clearText() {
|
||||
textArea.clear();
|
||||
textArea.forceActiveFocus();
|
||||
setTextAreaFocus();
|
||||
}
|
||||
|
||||
function setTextAreaFocus() {
|
||||
if (visible){
|
||||
textArea.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
radius: JamiTheme.primaryRadius
|
||||
@ -42,7 +48,7 @@ Rectangle {
|
||||
|
||||
onFocusChanged: {
|
||||
if (focus) {
|
||||
textArea.forceActiveFocus();
|
||||
setTextAreaFocus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,6 +36,36 @@ FocusScope {
|
||||
property color textColor: JamiTheme.textColor
|
||||
property color iconColor: JamiTheme.tintedBlue
|
||||
|
||||
Accessible.role: Accessible.Paragraph
|
||||
Accessible.name: {
|
||||
switch (type) {
|
||||
case "donation":
|
||||
return JamiStrings.donation
|
||||
case "backup":
|
||||
return JamiStrings.backupAccountBtn
|
||||
case "customize":
|
||||
return JamiStrings.customize
|
||||
case "tip":
|
||||
return title
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
Accessible.description: {
|
||||
switch (type) {
|
||||
case "donation":
|
||||
return JamiStrings.donationTipBoxText
|
||||
case "backup":
|
||||
return JamiStrings.whyBackupAccount
|
||||
case "customize":
|
||||
return JamiStrings.customizeText
|
||||
case "tip":
|
||||
return description
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
property string customizeTip: "CustomizeTipBox {}"
|
||||
|
||||
property string backupTip: "BackupTipBox {" + " onIgnore: {" + " root.ignoreClicked()" + " }" + "}"
|
||||
@ -75,6 +105,9 @@ FocusScope {
|
||||
active: type === "donation"
|
||||
focus: true
|
||||
sourceComponent: DonationTipBox {
|
||||
Accessible.name: JamiStrings.donation
|
||||
Accessible.description: JamiStrings.donationTipBoxText
|
||||
Accessible.role: Accessible.Link
|
||||
maxHeight: root.maximumHeight
|
||||
textColor: root.textColor
|
||||
iconColor: root.iconColor
|
||||
@ -86,6 +119,9 @@ FocusScope {
|
||||
id: loader_backupTip
|
||||
active: type === "backup"
|
||||
sourceComponent: BackupTipBox {
|
||||
Accessible.name: JamiStrings.backupAccountBtn
|
||||
Accessible.description: JamiStrings.whyBackupAccount
|
||||
Accessible.role: Accessible.Link
|
||||
onIgnore: {
|
||||
root.ignoreClicked();
|
||||
}
|
||||
@ -95,20 +131,24 @@ FocusScope {
|
||||
}
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader_customizeTip
|
||||
active: type === "customize"
|
||||
sourceComponent: CustomizeTipBox {
|
||||
Accessible.role: Accessible.Link
|
||||
textColor: root.textColor
|
||||
iconColor: root.iconColor
|
||||
}
|
||||
width: parent.width
|
||||
focus: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader_infoTip
|
||||
active: type === "tip"
|
||||
sourceComponent: InformativeTipBox {
|
||||
Accessible.role: Accessible.Link
|
||||
maxHeight: root.maximumHeight
|
||||
textColor: root.textColor
|
||||
iconColor: root.iconColor
|
||||
@ -159,6 +199,8 @@ FocusScope {
|
||||
id: component_btnClose
|
||||
PushButton {
|
||||
id: btnClose
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: JamiStrings.dismissTip
|
||||
|
||||
width: 20
|
||||
height: 20
|
||||
|
||||
@ -18,7 +18,6 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
import net.jami.Adapters 1.1
|
||||
import net.jami.Constants 1.1
|
||||
import net.jami.Enums 1.1
|
||||
@ -39,6 +38,10 @@ ListSelectionView {
|
||||
onPresented: LRCInstance.deselectConversation()
|
||||
leftPaneItem: viewCoordinator.getView("SidePanel", true)
|
||||
|
||||
Accessible.role: Accessible.Pane
|
||||
Accessible.name: title
|
||||
Accessible.description: JamiStrings.description
|
||||
|
||||
property variant uiCustomization: CurrentAccount.uiCustomization
|
||||
|
||||
onUiCustomizationChanged: {
|
||||
@ -277,6 +280,9 @@ ListSelectionView {
|
||||
preferredWidth: textSize.width + 2 * JamiTheme.preferredMarginSize
|
||||
text: JamiStrings.aboutJami
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: text
|
||||
|
||||
onClicked: viewCoordinator.presentDialog(appWindow, "mainview/components/AboutPopUp.qml")
|
||||
}
|
||||
|
||||
|
||||
@ -21,7 +21,6 @@
|
||||
#include "qtutils.h"
|
||||
#include "messageparser.h"
|
||||
#include "previewengine.h"
|
||||
#include "spellchecker.h"
|
||||
|
||||
#include <api/datatransfermodel.h>
|
||||
#include <api/contact.h>
|
||||
@ -40,25 +39,17 @@
|
||||
#include <QtMath>
|
||||
#include <QRegExp>
|
||||
|
||||
#define SUGGESTIONS_MAX_SIZE 3 // limit the number of spelling suggestions
|
||||
|
||||
MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
|
||||
PreviewEngine* previewEngine,
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager,
|
||||
LRCInstance* instance,
|
||||
QObject* parent)
|
||||
: QmlAdapterBase(instance, parent)
|
||||
, settingsManager_(settingsManager)
|
||||
, spellCheckDictionaryManager_(spellCheckDictionaryManager)
|
||||
, messageParser_(new MessageParser(previewEngine, this))
|
||||
, filteredMsgListModel_(new FilteredMsgListModel(this))
|
||||
, mediaInteractions_(std::make_unique<MessageListModel>(nullptr))
|
||||
, timestampTimer_(new QTimer(this))
|
||||
{
|
||||
#if defined(Q_OS_LINUX)
|
||||
// Initialize with make_shared
|
||||
spellChecker_ = std::make_shared<SpellChecker>(spellCheckDictionaryManager_->getDictionaryPath());
|
||||
#endif
|
||||
setObjectName(typeid(*this).name());
|
||||
|
||||
set_messageListModel(QVariant::fromValue(filteredMsgListModel_));
|
||||
@ -736,53 +727,3 @@ MessagesAdapter::getMsgListSourceModel() const
|
||||
// However it may be a nullptr if not yet set.
|
||||
return static_cast<MessageListModel*>(filteredMsgListModel_->sourceModel());
|
||||
}
|
||||
|
||||
bool
|
||||
MessagesAdapter::spell(const QString& word)
|
||||
{
|
||||
return spellChecker_->spell(word);
|
||||
}
|
||||
|
||||
QVariantList
|
||||
MessagesAdapter::spellSuggestionsRequest(const QString& word)
|
||||
{
|
||||
QStringList suggestionsList;
|
||||
QVariantList variantList;
|
||||
if (spellChecker_ == nullptr || spellChecker_->spell(word)) {
|
||||
return variantList;
|
||||
}
|
||||
|
||||
suggestionsList = spellChecker_->suggest(word);
|
||||
for (const auto& suggestion : suggestionsList) {
|
||||
if (variantList.size() >= SUGGESTIONS_MAX_SIZE) {
|
||||
break;
|
||||
}
|
||||
variantList.append(QVariant(suggestion));
|
||||
}
|
||||
|
||||
return variantList;
|
||||
}
|
||||
|
||||
QVariantList
|
||||
MessagesAdapter::findWords(const QString& text)
|
||||
{
|
||||
QVariantList result;
|
||||
if (!spellChecker_)
|
||||
return result;
|
||||
|
||||
auto words = spellChecker_->findWords(text);
|
||||
for (const auto& word : words) {
|
||||
QVariantMap wordInfo;
|
||||
wordInfo["word"] = word.word;
|
||||
wordInfo["position"] = word.position;
|
||||
wordInfo["length"] = word.length;
|
||||
result.append(wordInfo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void
|
||||
MessagesAdapter::updateDictionnary(const QString& path)
|
||||
{
|
||||
return spellChecker_->replaceDictionary(path);
|
||||
}
|
||||
|
||||
@ -23,8 +23,6 @@
|
||||
#include "previewengine.h"
|
||||
#include "messageparser.h"
|
||||
#include "appsettingsmanager.h"
|
||||
#include "spellchecker.h"
|
||||
#include "spellcheckdictionarymanager.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
@ -102,14 +100,11 @@ public:
|
||||
{
|
||||
return new MessagesAdapter(qApp->property("AppSettingsManager").value<AppSettingsManager*>(),
|
||||
qApp->property("PreviewEngine").value<PreviewEngine*>(),
|
||||
qApp->property("SpellCheckDictionaryManager")
|
||||
.value<SpellCheckDictionaryManager*>(),
|
||||
qApp->property("LRCInstance").value<LRCInstance*>());
|
||||
}
|
||||
|
||||
explicit MessagesAdapter(AppSettingsManager* settingsManager,
|
||||
PreviewEngine* previewEngine,
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager,
|
||||
LRCInstance* instance,
|
||||
QObject* parent = nullptr);
|
||||
~MessagesAdapter() = default;
|
||||
@ -168,10 +163,6 @@ public:
|
||||
Q_INVOKABLE QVariant dataForInteraction(const QString& interactionId,
|
||||
int role = Qt::DisplayRole) const;
|
||||
Q_INVOKABLE void startSearch(const QString& text, bool isMedia);
|
||||
Q_INVOKABLE QVariantList spellSuggestionsRequest(const QString& word);
|
||||
Q_INVOKABLE bool spell(const QString& word);
|
||||
Q_INVOKABLE void updateDictionnary(const QString& path);
|
||||
Q_INVOKABLE QVariantList findWords(const QString& text);
|
||||
|
||||
// Run corrsponding js functions, c++ to qml.
|
||||
void setMessagesImageContent(const QString& path, bool isBased64 = false);
|
||||
@ -206,12 +197,10 @@ private:
|
||||
QList<QString> conversationTypersUrlToName(const QSet<QString>& typersSet);
|
||||
|
||||
AppSettingsManager* settingsManager_;
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager_;
|
||||
MessageParser* messageParser_;
|
||||
FilteredMsgListModel* filteredMsgListModel_;
|
||||
std::unique_ptr<MessageListModel> mediaInteractions_;
|
||||
QTimer* timestampTimer_;
|
||||
std::shared_ptr<SpellChecker> spellChecker_;
|
||||
static constexpr const int loadChunkSize_ {20};
|
||||
static constexpr const int timestampUpdateIntervalMs_ {1000};
|
||||
};
|
||||
|
||||
@ -110,7 +110,8 @@ Item {
|
||||
property string enablePTT: qsTr("Enable push-to-talk")
|
||||
property string keyboardShortcut: qsTr("Keyboard shortcut")
|
||||
property string changeKeyboardShortcut: qsTr("Change keyboard shortcut")
|
||||
property string raiseWhenCalled: qsTr("Send the application to the front for incoming calls")
|
||||
property string raiseWhenCalled: qsTr("Bring the application to the front for incoming calls")
|
||||
property string denySecondCall: qsTr("Decline incoming calls when already in a call")
|
||||
|
||||
// ChangePttKeyPopup
|
||||
property string changeShortcut: qsTr("Change shortcut")
|
||||
@ -119,7 +120,7 @@ Item {
|
||||
|
||||
// AdvancedChatSettings
|
||||
property string enableReadReceipts: qsTr("Enable read receipts")
|
||||
property string enableReadReceiptsTooltip: qsTr("Send and receive receipts indicating that a message have been displayed")
|
||||
property string enableReadReceiptsTooltip: qsTr("Send and receive receipts indicating that messages have been displayed.")
|
||||
|
||||
// AdvancedVoiceMailSettings
|
||||
property string voiceMail: qsTr("Voicemail")
|
||||
@ -272,9 +273,9 @@ Item {
|
||||
property string hideSpectators: qsTr("Hide spectators")
|
||||
|
||||
// LineEditContextMenu
|
||||
property string copy: qsTr("Copy")
|
||||
property string share: qsTr("Share")
|
||||
property string cut: qsTr("Cut")
|
||||
property string copy: qsTr("Copy")
|
||||
property string paste: qsTr("Paste")
|
||||
property string language: qsTr("Language")
|
||||
|
||||
@ -399,8 +400,11 @@ Item {
|
||||
property string nameAlreadyTaken: qsTr("Name already taken")
|
||||
property string usernameAlreadyTaken: qsTr("Username already taken")
|
||||
property string joinJamiNoPassword: qsTr("Do you want to create a Jami account without a username?\nIf yes, only a randomly generated 40-character identifier will be assigned to the account.")
|
||||
property string usernameToolTip: qsTr("- 32 characters maximum\n- Alphabetical characters (A to Z and a to z)\n- Numeric characters (0 to 9)\n- Special characters allowed: dash (-)")
|
||||
|
||||
property string usernameToolTip: qsTr("- 32 characters maximum\n- Special characters allowed: dash (-)")
|
||||
property string customizeProfileOptional: qsTr("Customize your profile (optional)")
|
||||
property string skip: qsTr("Skip")
|
||||
property string skipProfile: qsTr("Skip profile configuration")
|
||||
property string saveProfile: qsTr("Save profile")
|
||||
// Good to know
|
||||
property string goodToKnow: qsTr("Good to know")
|
||||
property string local: qsTr("Local")
|
||||
@ -408,7 +412,7 @@ Item {
|
||||
property string localAccount: qsTr("Your account will be created and stored locally.")
|
||||
property string usernameRecommened: qsTr("Choosing a username is recommended, and a chosen username CANNOT be changed later.")
|
||||
property string passwordOptional: qsTr("Encrypting your account with a password is optional, and if the password is lost it CANNOT be recovered later.")
|
||||
property string customizeOptional: qsTr("Setting a profile picture and nickname is optional, and can also be changed later in the settings.")
|
||||
property string customizeOptional: qsTr("Setting a profile picture and display name is optional, and can also be changed later in the settings.")
|
||||
|
||||
// CreateSIPAccountPage
|
||||
property string sipAccount: qsTr("SIP account")
|
||||
@ -422,7 +426,7 @@ Item {
|
||||
property string displayName: qsTr("Display name")
|
||||
|
||||
// accountSettingsPages
|
||||
property string customizeAccountDescription: qsTr("Your profile is only shared with your contacts.\nYour picture and your nickname can be changed at all time in the settings of your account.")
|
||||
property string customizeAccountDescription: qsTr("Your profile is only shared with your contacts.\nYour picture and your display name can be changed at all time in the settings of your account.")
|
||||
property string usernameAccountDescription: qsTr("A chosen username can help to be found more easily on Jami.\nIf a username is not chosen, a randomly generated 40-character identifier will be assigned to this account as a username. It is more difficult to be found and reached with this identifier.")
|
||||
property string encryptAccountDescription: qsTr("Your Jami account is registered only on this device as an archive containing the keys of your account. Access to this archive can be protected with a password.")
|
||||
property string saveAccountTitle: qsTr("Backup account")
|
||||
@ -449,7 +453,6 @@ Item {
|
||||
property string advancedAccountSettings: qsTr("Advanced account settings")
|
||||
property string encryptAccount: qsTr("Encrypt account with password")
|
||||
property string customizeProfile: qsTr("Customize profile")
|
||||
property string customizeProfileDescription: qsTr("This profile is only shared with account contacts.\nThe profile can be changed in account settings.")
|
||||
property string encryptTitle: qsTr("Encrypt account with password")
|
||||
property string encryptDescription: qsTr("A Jami account is created and stored locally only on this device, as an archive containing your account keys. Access to this archive can optionally be protected with a password.")
|
||||
property string encryptWarning: qsTr("Please note that if you lose your password, it CANNOT be recovered!")
|
||||
@ -506,7 +509,7 @@ Item {
|
||||
|
||||
// ChatviewSettings
|
||||
property string enableTypingIndicator: qsTr("Enable typing indicators")
|
||||
property string enableTypingIndicatorDescription: qsTr("Send and receive typing indicators showing that a message is being typed.")
|
||||
property string enableTypingIndicatorDescription: qsTr("Send and receive typing indicators showing when messages are being typed.")
|
||||
property string displayHyperlinkPreviews: qsTr("Web link previews")
|
||||
property string displayHyperlinkPreviewsDescription: qsTr("Preview requires downloading content from third-party servers.")
|
||||
|
||||
@ -605,8 +608,8 @@ Item {
|
||||
property string linkNewDevice: qsTr("Link new device")
|
||||
property string linkDeviceConnecting: qsTr("Connecting to your new device…")
|
||||
property string linkDeviceInProgress: qsTr("The export account operation to the new device is in progress.\nPlease confirm the import on the new device.")
|
||||
property string linkDeviceScanQR: qsTr("On the new device, initiate a new account.\nSelect Add account -> Connect from another device.\nWhen ready, scan the QR code.")
|
||||
property string linkDeviceEnterManually: qsTr("Alternatively you could enter the authentication code manually.")
|
||||
property string linkDeviceScanQR: qsTr("On the new device, initiate a new account.\nSelect Add account → Connect from another device.\nWhen ready, scan the QR code.")
|
||||
property string linkDeviceEnterManually: qsTr("Alternatively, enter the authentication code manually.")
|
||||
property string linkDeviceEnterCodePlaceholder: qsTr("Enter authentication code")
|
||||
property string linkDeviceAllSet: qsTr("The account was imported successfully.")
|
||||
property string linkDeviceFoundAddress: qsTr("New device found at address below. Is that you?\nClicking on confirm will continue transfering account.")
|
||||
@ -786,7 +789,7 @@ Item {
|
||||
property string replyTo: qsTr("Reply to")
|
||||
property string inReplyTo: qsTr("In reply to")
|
||||
property string repliedTo: qsTr("%1 replied to")
|
||||
property string inReplyToMe: qsTr("Me")
|
||||
property string inReplyToYou: qsTr("you")
|
||||
property string reply: qsTr("Reply")
|
||||
property string writeTo: qsTr("Write to %1")
|
||||
property string writeToNewContact: qsTr("Send a message to %1 in order to add them as a contact")
|
||||
@ -908,11 +911,44 @@ Item {
|
||||
property string remote: qsTr("Remote: %1")
|
||||
property string view: qsTr("View")
|
||||
|
||||
// Spellchecker
|
||||
// Spell checker
|
||||
property string checkSpelling: qsTr("Check spelling while typing")
|
||||
property string systemDictionary: qsTr("System")
|
||||
property string textLanguage: qsTr("Text language")
|
||||
property string textLanguageDescription: qsTr("To install new dictionaries, use the system package manager.")
|
||||
property string spellchecking: qsTr("Spellchecking")
|
||||
property string refresh: qsTr("Refresh")
|
||||
property string refreshInstalledDictionaries: qsTr("Refresh installed dictionaries")
|
||||
property string spellChecker: qsTr("Spell checker")
|
||||
property string searchTextLanguages: qsTr("Search text languages")
|
||||
property string searchAvailableTextLanguages: qsTr("Search for available text languages")
|
||||
property string noDictionariesFoundFor: qsTr("No dictionaries found for '%1'")
|
||||
property string noDictionariesAvailable: qsTr("No dictionaries available")
|
||||
property string manageDictionaries: qsTr("Manage Dictionaries")
|
||||
property string spellCheckDownloadFailed: qsTr("Download failed for dictionary '%1'")
|
||||
property string showInstalledDictionaries: qsTr("Show installed")
|
||||
property string showInstalledDictionariesDescription: qsTr("Only show dictionaries that are currently installed")
|
||||
|
||||
// Accessibility
|
||||
property string switchToAccount: qsTr("Press enter to switch to this account")
|
||||
property string qrCodeExplanation: qsTr("Display your QR code to allow other users to scan it and add you as a contact")
|
||||
property string accountList: qsTr("Account list")
|
||||
property string accountListDescription: qsTr("Use arrows to switch between available account")
|
||||
property string languageComboBoxExplanation: qsTr("Select the user interface language")
|
||||
property string backButtonExplanation: qsTr("Go back to the previous page")
|
||||
property string adviceBox: qsTr("Advice Box")
|
||||
property string backButton: qsTr("Back button")
|
||||
property string adviceBoxExplanation: qsTr("Open the advice popup that contains information about Jami")
|
||||
property string more: qsTr("More")
|
||||
property string pressToAction: qsTr("Press to %1")
|
||||
property string pressToToggle: qsTr("Press to toggle %1 (%2)")
|
||||
property string active: qsTr("active")
|
||||
property string inactive: qsTr("inactive")
|
||||
property string minimize: qsTr("Minimize application")
|
||||
property string maximize: qsTr("Maximize application")
|
||||
property string closeApplication: qsTr("Close application")
|
||||
property string dismissTip: qsTr("Dismiss this tip")
|
||||
property string tipDescription: qsTr("Tips to help you use Jami more effectively")
|
||||
property string showMoreMessagingOptions: qsTr("Show more messaging options")
|
||||
property string showMoreMessagingOptionsDescription: qsTr("Open a menu that allows you to send voice and video messages as well as sharing your location")
|
||||
property string conversationMessages: qsTr("Conversation messages list. Use arrow keys to navigate through messages.")
|
||||
property string dataTransfer: qsTr("Data transfer")
|
||||
property string status: qsTr("Status")
|
||||
property string readBy: qsTr("Read by")
|
||||
}
|
||||
|
||||
@ -83,6 +83,7 @@ Item {
|
||||
property color editBackgroundColor: darkTheme ? "#373737" : lightGrey_
|
||||
property color textColor: primaryForegroundColor
|
||||
property color textColorHovered: darkTheme ? "#cccccc" : "#333333"
|
||||
property color textColorHoveredHighContrast: darkTheme ? "#6a6a6a" : "#a7a7a7"
|
||||
property color tabbarBorderColor: darkTheme ? blackColor : "#e3e3e3"
|
||||
property color popupOverlayColor: darkTheme ? Qt.rgba(255, 255, 255, 0.22) : Qt.rgba(0, 0, 0, 0.33)
|
||||
property real formsRadius: 30
|
||||
@ -103,7 +104,7 @@ Item {
|
||||
property color pressedButtonColor: darkTheme ? pressColor : "#a0a0a0"
|
||||
property color hoveredButtonColor: darkTheme ? "#4d4d4d" : "#dedede"
|
||||
property color hoveredButtonColorWizard: darkTheme ? "#4d4d4d" : "#dedede"
|
||||
property color normalButtonColor: darkTheme ? backgroundColor : "#e0e0e0"
|
||||
property color normalButtonColor: darkTheme ? "#bfbfbf" : "#e0e0e0"
|
||||
|
||||
property color invertedPressedButtonColor: Qt.rgba(0, 0, 0, 0.5)
|
||||
property color invertedHoveredButtonColor: Qt.rgba(0, 0, 0, 0.6)
|
||||
|
||||
@ -36,7 +36,8 @@
|
||||
#include "currentaccounttomigrate.h"
|
||||
#include "pttlistener.h"
|
||||
#include "calloverlaymodel.h"
|
||||
#include "spellcheckdictionarymanager.h"
|
||||
#include "spellcheckdictionarylistmodel.h"
|
||||
#include "spellcheckadapter.h"
|
||||
#include "accountlistmodel.h"
|
||||
#include "mediacodeclistmodel.h"
|
||||
#include "audiodevicemodel.h"
|
||||
@ -65,7 +66,6 @@
|
||||
#include "wizardviewstepmodel.h"
|
||||
#include "linkdevicemodel.h"
|
||||
#include "qrcodescannermodel.h"
|
||||
#include "spellchecker.h"
|
||||
|
||||
#include "api/peerdiscoverymodel.h"
|
||||
#include "api/codecmodel.h"
|
||||
@ -119,7 +119,6 @@ registerTypes(QQmlEngine* engine,
|
||||
LRCInstance* lrcInstance,
|
||||
SystemTray* systemTray,
|
||||
AppSettingsManager* settingsManager,
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager,
|
||||
ConnectivityMonitor* connectivityMonitor,
|
||||
PreviewEngine* previewEngine,
|
||||
ScreenInfo* screenInfo,
|
||||
@ -196,6 +195,10 @@ registerTypes(QQmlEngine* engine,
|
||||
QQmlEngine::setObjectOwnership(linkdevicemodel, QQmlEngine::CppOwnership);
|
||||
REG_QML_SINGLETON<LinkDeviceModel>(REG_MODEL, "LinkDeviceModel", CREATE(linkdevicemodel));
|
||||
|
||||
// SpellCheckDictionaryListModel (available through SpellCheckAdapter)
|
||||
auto spellCheckDictionaryListModel = new SpellCheckDictionaryListModel(settingsManager, connectivityMonitor, app);
|
||||
qApp->setProperty("SpellCheckDictionaryListModel", QVariant::fromValue(spellCheckDictionaryListModel));
|
||||
|
||||
// Register app-level objects that are used by QML created objects.
|
||||
// These MUST be set prior to loading the initial QML file, in order to
|
||||
// be available to the QML adapter class factory creation methods.
|
||||
@ -204,7 +207,6 @@ registerTypes(QQmlEngine* engine,
|
||||
qApp->setProperty("AppSettingsManager", QVariant::fromValue(settingsManager));
|
||||
qApp->setProperty("ConnectivityMonitor", QVariant::fromValue(connectivityMonitor));
|
||||
qApp->setProperty("PreviewEngine", QVariant::fromValue(previewEngine));
|
||||
qApp->setProperty("SpellCheckDictionaryManager", QVariant::fromValue(spellCheckDictionaryManager));
|
||||
|
||||
// qml adapter registration
|
||||
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, QRCodeScannerModel);
|
||||
@ -225,6 +227,7 @@ registerTypes(QQmlEngine* engine,
|
||||
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, VideoDevices);
|
||||
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, CurrentAccountToMigrate);
|
||||
QML_REGISTERSINGLETON_TYPE(NS_HELPERS, FileDownloader);
|
||||
QML_REGISTERSINGLETON_TYPE(NS_ADAPTERS, SpellCheckAdapter);
|
||||
|
||||
// TODO: remove these
|
||||
QML_REGISTERSINGLETONTYPE_CUSTOM(NS_MODELS, AVModel, &lrcInstance->avModel())
|
||||
@ -241,7 +244,6 @@ registerTypes(QQmlEngine* engine,
|
||||
QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel);
|
||||
QML_REGISTERTYPE(NS_MODELS, FilesToSendListModel);
|
||||
QML_REGISTERTYPE(NS_MODELS, CallInformationListModel);
|
||||
QML_REGISTERTYPE(NS_MODELS, SpellChecker);
|
||||
|
||||
// Roles & type enums for models
|
||||
QML_REGISTERNAMESPACE(NS_MODELS, AccountList::staticMetaObject, "AccountList");
|
||||
@ -250,12 +252,12 @@ registerTypes(QQmlEngine* engine,
|
||||
QML_REGISTERNAMESPACE(NS_MODELS, FilesToSend::staticMetaObject, "FilesToSend");
|
||||
QML_REGISTERNAMESPACE(NS_MODELS, MessageList::staticMetaObject, "MessageList");
|
||||
QML_REGISTERNAMESPACE(NS_MODELS, PluginStatus::staticMetaObject, "PluginStatus");
|
||||
// QML_REGISTERNAMESPACE(NS_MODELS, SpellCheckDictionaryList::staticMetaObject, "SpellCheckDictionaryList");
|
||||
|
||||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, app, "MainApplication")
|
||||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, screenInfo, "CurrentScreenInfo")
|
||||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, lrcInstance, "LRCInstance")
|
||||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, settingsManager, "AppSettingsManager")
|
||||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, spellCheckDictionaryManager, "SpellCheckDictionaryManager")
|
||||
|
||||
// Lrc namespaces, models, and singletons
|
||||
QML_REGISTERNAMESPACE(NS_MODELS, lrc::api::staticMetaObject, "Lrc");
|
||||
|
||||
@ -32,7 +32,6 @@
|
||||
class SystemTray;
|
||||
class LRCInstance;
|
||||
class AppSettingsManager;
|
||||
class SpellCheckDictionaryManager;
|
||||
class PreviewEngine;
|
||||
class ScreenInfo;
|
||||
class MainApplication;
|
||||
@ -62,7 +61,6 @@ void registerTypes(QQmlEngine* engine,
|
||||
LRCInstance* lrcInstance,
|
||||
SystemTray* systemTray,
|
||||
AppSettingsManager* appSettingsManager,
|
||||
SpellCheckDictionaryManager* spellCheckDictionaryManager,
|
||||
ConnectivityMonitor* connectivityMonitor,
|
||||
PreviewEngine* previewEngine,
|
||||
ScreenInfo* screenInfo,
|
||||
|
||||
@ -265,11 +265,6 @@ SidePanelBase {
|
||||
|
||||
header: AccountComboBox { QWKSetParentHitTestVisible {}
|
||||
id: accountComboBox
|
||||
Shortcut {
|
||||
sequence: "Ctrl+J"
|
||||
context: Qt.ApplicationShortcut
|
||||
onActivated: accountComboBox.togglePopup()
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
|
||||
@ -27,7 +27,6 @@ import "../../commoncomponents"
|
||||
import "../../mainview/components"
|
||||
import "../../mainview/js/contactpickercreation.js" as ContactPickerCreation
|
||||
|
||||
|
||||
SettingsPageBase {
|
||||
id: root
|
||||
|
||||
@ -103,6 +102,14 @@ SettingsPageBase {
|
||||
checked: UtilsAdapter.getAppValue(Settings.RaiseWhenCalled)
|
||||
onSwitchToggled: UtilsAdapter.setAppValue(Settings.Key.RaiseWhenCalled, checked)
|
||||
}
|
||||
|
||||
ToggleSwitch {
|
||||
id: checkBoxDenySecondCall
|
||||
|
||||
labelText: JamiStrings.denySecondCall
|
||||
checked: CurrentAccount.denySecondCall
|
||||
onSwitchToggled: CurrentAccount.denySecondCall = checked
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@ -383,7 +390,7 @@ SettingsPageBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
ColumnLayout{
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: 9
|
||||
Text {
|
||||
@ -400,7 +407,7 @@ SettingsPageBase {
|
||||
labelText: JamiStrings.enablePTT
|
||||
checked: UtilsAdapter.getAppValue(Settings.EnablePtt)
|
||||
onSwitchToggled: {
|
||||
UtilsAdapter.setAppValue(Settings.Key.EnablePtt, checked)
|
||||
UtilsAdapter.setAppValue(Settings.Key.EnablePtt, checked);
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
@ -426,13 +433,13 @@ SettingsPageBase {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
background: Rectangle {
|
||||
id: backgroundRect
|
||||
anchors.centerIn: parent
|
||||
width: keyLabel.width + 2 * JamiTheme.preferredMarginSize
|
||||
height: keyLabel.height + JamiTheme.preferredMarginSize
|
||||
color: JamiTheme.lightGrey_
|
||||
border.color: JamiTheme.darkGreyColor
|
||||
radius: 4
|
||||
id: backgroundRect
|
||||
anchors.centerIn: parent
|
||||
width: keyLabel.width + 2 * JamiTheme.preferredMarginSize
|
||||
height: keyLabel.height + JamiTheme.preferredMarginSize
|
||||
color: JamiTheme.lightGrey_
|
||||
border.color: JamiTheme.darkGreyColor
|
||||
radius: 4
|
||||
}
|
||||
}
|
||||
MaterialButton {
|
||||
@ -444,8 +451,8 @@ SettingsPageBase {
|
||||
onClicked: {
|
||||
var dlg = viewCoordinator.presentDialog(appWindow, "commoncomponents/ChangePttKeyPopup.qml");
|
||||
dlg.choiceMade.connect(function (chosenKey) {
|
||||
keyLabel.text = PTTListener.keyToString(chosenKey);
|
||||
});
|
||||
keyLabel.text = PTTListener.keyToString(chosenKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,11 +17,13 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import net.jami.Models 1.1
|
||||
import net.jami.Adapters 1.1
|
||||
import net.jami.Enums 1.1
|
||||
import net.jami.Constants 1.1
|
||||
import net.jami.Helpers 1.1
|
||||
import SortFilterProxyModel 0.2
|
||||
import "../../commoncomponents"
|
||||
import "../../mainview/components"
|
||||
import "../../mainview/js/contactpickercreation.js" as ContactPickerCreation
|
||||
@ -41,6 +43,63 @@ SettingsPageBase {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: JamiTheme.preferredSettingsMarginSize
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
width: parent.width
|
||||
spacing: JamiTheme.settingsCategorySpacing
|
||||
|
||||
Text {
|
||||
id: spellCheckerTitle
|
||||
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.preferredWidth: parent.width
|
||||
|
||||
text: JamiStrings.spellChecker
|
||||
color: JamiTheme.textColor
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
font.pixelSize: JamiTheme.settingsTitlePixelSize
|
||||
font.kerning: true
|
||||
}
|
||||
|
||||
ToggleSwitch {
|
||||
id: enableSpellCheckToggleSwitch
|
||||
Layout.fillWidth: true
|
||||
visible: true
|
||||
|
||||
checked: UtilsAdapter.getAppValue(Settings.Key.EnableSpellCheck)
|
||||
labelText: JamiStrings.checkSpelling
|
||||
tooltipText: JamiStrings.checkSpelling
|
||||
onSwitchToggled: {
|
||||
UtilsAdapter.setAppValue(Settings.Key.EnableSpellCheck, checked);
|
||||
}
|
||||
}
|
||||
|
||||
SpellCheckLanguageComboBox {
|
||||
id: spellCheckLangComboBoxSetting
|
||||
Layout.fillWidth: true
|
||||
widthOfComboBox: itemWidth
|
||||
}
|
||||
|
||||
// A button to open the dictionary install view as a popup
|
||||
MaterialButton {
|
||||
id: dictionaryInstallButton
|
||||
|
||||
secondary: true
|
||||
|
||||
preferredWidth: itemWidth
|
||||
height: spellCheckLangComboBoxSetting.comboBox.height
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
text: JamiStrings.manageDictionaries
|
||||
onClicked: {
|
||||
viewCoordinator.presentDialog(appWindow, "commoncomponents/ManageDictionariesDialog.qml");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: generalSettings
|
||||
|
||||
|
||||
@ -38,6 +38,7 @@ SettingsPageBase {
|
||||
flickableContent: ColumnLayout {
|
||||
id: manageAccountColumnLayout
|
||||
|
||||
width: contentFlickableWidth
|
||||
spacing: JamiTheme.settingsBlockSpacing
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: JamiTheme.preferredSettingsMarginSize
|
||||
|
||||
@ -32,6 +32,7 @@ RowLayout {
|
||||
property alias fontPointSize: comboBoxOfLayout.font.pointSize
|
||||
property alias modelIndex: comboBoxOfLayout.currentIndex
|
||||
property alias modelSize: comboBoxOfLayout.count
|
||||
property alias comboBox: comboBoxOfLayout
|
||||
|
||||
property int widthOfComboBox: 50
|
||||
|
||||
@ -53,6 +54,7 @@ RowLayout {
|
||||
SettingParaCombobox {
|
||||
id: comboBoxOfLayout
|
||||
|
||||
enabled: root.enabled
|
||||
Layout.preferredWidth: widthOfComboBox
|
||||
|
||||
font.pointSize: JamiTheme.buttonFontSize
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Savoir-faire Linux Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import net.jami.Models 1.1
|
||||
import net.jami.Adapters 1.1
|
||||
import net.jami.Enums 1.1
|
||||
import net.jami.Constants 1.1
|
||||
import net.jami.Helpers 1.1
|
||||
import SortFilterProxyModel 0.2
|
||||
import "../../commoncomponents"
|
||||
import "../../mainview/components"
|
||||
|
||||
SettingsComboBox {
|
||||
id: root
|
||||
height: JamiTheme.preferredFieldHeight
|
||||
labelText: JamiStrings.textLanguage
|
||||
tipText: JamiStrings.textLanguage
|
||||
comboModel: SortFilterProxyModel {
|
||||
id: installedDictionariesModel
|
||||
sourceModel: SpellCheckAdapter.getDictionaryListModel()
|
||||
|
||||
// Filter to show only installed dictionaries
|
||||
filters: ValueFilter {
|
||||
roleName: "Installed"
|
||||
value: true
|
||||
}
|
||||
|
||||
// Sort alphabetically by native name
|
||||
sorters: RoleSorter {
|
||||
roleName: "NativeName"
|
||||
sortOrder: Qt.AscendingOrder
|
||||
}
|
||||
Component.onCompleted: {
|
||||
// Ensure the model is updated with the latest dictionaries
|
||||
root.enabled = Qt.binding(function () {
|
||||
return installedDictionariesModel.count > 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
role: "NativeName"
|
||||
|
||||
// Show placeholder when disabled
|
||||
placeholderText: JamiStrings.none
|
||||
|
||||
function getCurrentLocaleIndex() {
|
||||
var currentLang = UtilsAdapter.getAppValue(Settings.Key.SpellLang);
|
||||
for (var i = 0; i < comboModel.count; i++) {
|
||||
var item = comboModel.get(i);
|
||||
if (item.Locale === currentLang)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Set initial selection based on current spell language setting
|
||||
Component.onCompleted: modelIndex = getCurrentLocaleIndex()
|
||||
|
||||
property string locale
|
||||
function setForIndex(index) {
|
||||
var selectedDict = comboModel.get(index);
|
||||
if (selectedDict && selectedDict.Locale && selectedDict.Installed) {
|
||||
locale = selectedDict.Locale;
|
||||
}
|
||||
}
|
||||
onLocaleChanged: SpellCheckAdapter.setDictionary(locale)
|
||||
|
||||
// When the count changes, we might need to update the model index
|
||||
readonly property int count: installedDictionariesModel.count
|
||||
onCountChanged: {
|
||||
modelIndex = getCurrentLocaleIndex();
|
||||
// If the new index is -1 and we still have dictionaries, use the first one
|
||||
if (modelIndex === -1 && installedDictionariesModel.count > 0) {
|
||||
modelIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If the model index changes programmatically, we need to update the dictionary path
|
||||
onModelIndexChanged: setForIndex(modelIndex)
|
||||
}
|
||||
@ -185,165 +185,6 @@ SettingsPageBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
|
||||
width: parent.width
|
||||
spacing: JamiTheme.settingsCategorySpacing
|
||||
visible: (Qt.platform.os.toString() !== "linux") ? false : true
|
||||
|
||||
Text {
|
||||
id: spellcheckingTitle
|
||||
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.preferredWidth: parent.width
|
||||
|
||||
text: JamiStrings.spellchecking
|
||||
color: JamiTheme.textColor
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
font.pixelSize: JamiTheme.settingsTitlePixelSize
|
||||
font.kerning: true
|
||||
}
|
||||
|
||||
ToggleSwitch {
|
||||
id: enableSpellCheckToggleSwitch
|
||||
Layout.fillWidth: true
|
||||
visible: true
|
||||
|
||||
checked: UtilsAdapter.getAppValue(Settings.Key.EnableSpellCheck)
|
||||
labelText: JamiStrings.checkSpelling
|
||||
descText: JamiStrings.textLanguageDescription
|
||||
tooltipText: JamiStrings.checkSpelling
|
||||
onSwitchToggled: {
|
||||
UtilsAdapter.setAppValue(Settings.Key.EnableSpellCheck, checked);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsComboBox {
|
||||
id: spellCheckLangComboBoxSetting
|
||||
|
||||
Layout.fillWidth: true
|
||||
height: JamiTheme.preferredFieldHeight
|
||||
|
||||
labelText: JamiStrings.textLanguage
|
||||
tipText: JamiStrings.textLanguage
|
||||
comboModel: ListModel {
|
||||
id: installedSpellCheckLangModel
|
||||
Component.onCompleted: {
|
||||
var supported = SpellCheckDictionaryManager.installedDictionaries();
|
||||
var keys = Object.keys(supported);
|
||||
var currentKey = UtilsAdapter.getAppValue(Settings.Key.SpellLang);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
append({
|
||||
"textDisplay": supported[keys[i]],
|
||||
"id": keys[i]
|
||||
});
|
||||
if (keys[i] === currentKey)
|
||||
spellCheckLangComboBoxSetting.modelIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
widthOfComboBox: itemWidth
|
||||
role: "textDisplay"
|
||||
|
||||
onActivated: {
|
||||
UtilsAdapter.setAppValue(Settings.Key.SpellLang, comboModel.get(modelIndex).id);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: JamiTheme.preferredFieldHeight
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.rightMargin: JamiTheme.preferredMarginSize
|
||||
|
||||
color: JamiTheme.textColor
|
||||
wrapMode: Text.WordWrap
|
||||
text: JamiStrings.refreshInstalledDictionaries
|
||||
font.pointSize: JamiTheme.settingsFontSize
|
||||
font.kerning: true
|
||||
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
MaterialButton {
|
||||
id: refreshInstalledDictionariesPushButton
|
||||
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
preferredWidth: textSizeRefresh.width + 2 * JamiTheme.buttontextWizzardPadding
|
||||
buttontextHeightMargin: JamiTheme.buttontextHeightMargin
|
||||
|
||||
primary: true
|
||||
toolTipText: JamiStrings.refresh
|
||||
|
||||
text: JamiStrings.refresh
|
||||
|
||||
onClicked: {
|
||||
SpellCheckDictionaryManager.refreshDictionaries();
|
||||
var langIdx = spellCheckLangComboBoxSetting.modelIndex;
|
||||
installedSpellCheckLangModel.clear();
|
||||
var supported = SpellCheckDictionaryManager.installedDictionaries();
|
||||
var keys = Object.keys(supported);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
installedSpellCheckLangModel.append({
|
||||
"textDisplay": supported[keys[i]],
|
||||
"id": keys[i]
|
||||
});
|
||||
}
|
||||
spellCheckLangComboBoxSetting.modelIndex = langIdx;
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: textSizeRefresh
|
||||
font.weight: Font.Bold
|
||||
font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize
|
||||
font.capitalization: Font.AllUppercase
|
||||
text: refreshInstalledDictionariesPushButton.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: UtilsAdapter
|
||||
|
||||
function onChangeLanguage() {
|
||||
var langIdx = langComboBoxSetting.modelIndex;
|
||||
langModel.clear();
|
||||
var supported = UtilsAdapter.supportedLang();
|
||||
var keys = Object.keys(supported);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
langModel.append({
|
||||
"textDisplay": supported[keys[i]],
|
||||
"id": keys[i]
|
||||
});
|
||||
}
|
||||
langComboBoxSetting.modelIndex = langIdx;
|
||||
}
|
||||
|
||||
// Repopulate the spell check language list
|
||||
function onSpellLanguageChanged() {
|
||||
var langIdx = spellCheckLangComboBoxSetting.modelIndex;
|
||||
installedSpellCheckLangModel.clear();
|
||||
var supported = SpellCheckDictionaryManager.installedDictionaries();
|
||||
var keys = Object.keys(supported);
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
installedSpellCheckLangModel.append({
|
||||
"textDisplay": supported[keys[i]],
|
||||
"id": keys[i]
|
||||
});
|
||||
}
|
||||
spellCheckLangComboBoxSetting.modelIndex = langIdx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
||||
|
||||
142
src/app/spellcheckadapter.cpp
Normal file
142
src/app/spellcheckadapter.cpp
Normal file
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Savoir-faire Linux Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "spellcheckadapter.h"
|
||||
|
||||
#include "spellcheckdictionarylistmodel.h"
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
#define SUGGESTIONS_MAX_SIZE 10 // limit the number of spelling suggestions
|
||||
|
||||
SpellCheckAdapter::SpellCheckAdapter(SpellCheckDictionaryListModel* dictionaryListModel,
|
||||
AppSettingsManager* settingsManager,
|
||||
QObject* parent)
|
||||
: QObject(parent)
|
||||
, dictionaryListModel_(dictionaryListModel)
|
||||
, settingsManager_(settingsManager)
|
||||
{
|
||||
// Connect to update the selected dictionary if no dictionary is set on start
|
||||
connect(dictionaryListModel_,
|
||||
&SpellCheckDictionaryListModel::newDictionaryAvailable,
|
||||
this,
|
||||
&SpellCheckAdapter::setDictionary);
|
||||
|
||||
// Load the current dictionary if available
|
||||
auto currentLocale = settingsManager_->getValue(Settings::Key::SpellLang).toString();
|
||||
if (!currentLocale.isEmpty()) {
|
||||
setDictionary(currentLocale);
|
||||
}
|
||||
|
||||
// Listen for data changes to the dictionaryListModel_ to determine
|
||||
// our installedDictionaryCount
|
||||
connect(dictionaryListModel_,
|
||||
&QAbstractItemModel::dataChanged,
|
||||
this,
|
||||
[this](const QModelIndex&, const QModelIndex&, const QList<int>& roles) {
|
||||
if (roles.contains(SpellCheckDictionaryList::Installed)) {
|
||||
set_installedDictionaryCount(
|
||||
dictionaryListModel_->getInstalledDictionaries().size());
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize the installedDictionaryCount
|
||||
set_installedDictionaryCount(dictionaryListModel_->getInstalledDictionaries().size());
|
||||
|
||||
// Listen for download failure
|
||||
connect(dictionaryListModel_,
|
||||
&SpellCheckDictionaryListModel::downloadFailed,
|
||||
this,
|
||||
&SpellCheckAdapter::downloadFailed);
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckAdapter::installDictionary(const QString& locale)
|
||||
{
|
||||
dictionaryListModel_->installDictionary(locale);
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckAdapter::uninstallDictionary(const QString& locale)
|
||||
{
|
||||
dictionaryListModel_->uninstallDictionary(locale);
|
||||
}
|
||||
|
||||
QVariant
|
||||
SpellCheckAdapter::getDictionaryListModel() const
|
||||
{
|
||||
return QVariant::fromValue(dictionaryListModel_);
|
||||
}
|
||||
|
||||
QVariantMap
|
||||
SpellCheckAdapter::getInstalledDictionaries() const
|
||||
{
|
||||
return dictionaryListModel_->getInstalledDictionaries();
|
||||
}
|
||||
|
||||
bool
|
||||
SpellCheckAdapter::spell(const QString& word)
|
||||
{
|
||||
return spellChecker_.spell(word);
|
||||
}
|
||||
|
||||
QVariantList
|
||||
SpellCheckAdapter::spellSuggestionsRequest(const QString& word)
|
||||
{
|
||||
QStringList suggestionsList;
|
||||
QVariantList variantList;
|
||||
if (spellChecker_.spell(word)) {
|
||||
return variantList;
|
||||
}
|
||||
|
||||
suggestionsList = spellChecker_.suggest(word);
|
||||
for (const auto& suggestion : suggestionsList) {
|
||||
if (variantList.size() >= SUGGESTIONS_MAX_SIZE) {
|
||||
break;
|
||||
}
|
||||
variantList.append(QVariant(suggestion));
|
||||
}
|
||||
return variantList;
|
||||
}
|
||||
|
||||
QVariantList
|
||||
SpellCheckAdapter::findWords(const QString& text)
|
||||
{
|
||||
QVariantList result;
|
||||
auto words = spellChecker_.findWords(text);
|
||||
for (const auto& word : words) {
|
||||
QVariantMap wordInfo;
|
||||
wordInfo["word"] = word.word;
|
||||
wordInfo["position"] = word.position;
|
||||
wordInfo["length"] = word.length;
|
||||
result.append(wordInfo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckAdapter::setDictionary(const QString& locale)
|
||||
{
|
||||
auto localPath = dictionaryListModel_->pathForLocale(locale);
|
||||
if (spellChecker_.replaceDictionary(localPath)) {
|
||||
settingsManager_->setValue(Settings::Key::SpellLang, locale);
|
||||
set_hasLoadedDictionary(true);
|
||||
Q_EMIT dictionaryChanged();
|
||||
} else {
|
||||
qWarning() << "Failed to set dictionary for locale:" << locale;
|
||||
}
|
||||
}
|
||||
73
src/app/spellcheckadapter.h
Normal file
73
src/app/spellcheckadapter.h
Normal file
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Savoir-faire Linux Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "spellchecker.h"
|
||||
#include "qtutils.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine> // QML registration
|
||||
#include <QApplication> // QML registration
|
||||
|
||||
class SpellCheckDictionaryListModel;
|
||||
class AppSettingsManager;
|
||||
|
||||
class SpellCheckAdapter final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_SINGLETON
|
||||
|
||||
QML_RO_PROPERTY(int, installedDictionaryCount)
|
||||
QML_RO_PROPERTY(bool, hasLoadedDictionary)
|
||||
|
||||
public:
|
||||
static SpellCheckAdapter* create(QQmlEngine* engine, QJSEngine*)
|
||||
{
|
||||
return new SpellCheckAdapter(
|
||||
qApp->property("SpellCheckDictionaryListModel").value<SpellCheckDictionaryListModel*>(),
|
||||
qApp->property("AppSettingsManager").value<AppSettingsManager*>(),
|
||||
engine);
|
||||
}
|
||||
|
||||
explicit SpellCheckAdapter(SpellCheckDictionaryListModel* dictionaryListModel,
|
||||
AppSettingsManager* settingsManager,
|
||||
QObject* parent = nullptr);
|
||||
~SpellCheckAdapter() = default;
|
||||
|
||||
Q_INVOKABLE QVariant getDictionaryListModel() const;
|
||||
Q_INVOKABLE QVariantMap getInstalledDictionaries() const;
|
||||
|
||||
Q_INVOKABLE void installDictionary(const QString& locale);
|
||||
Q_INVOKABLE void uninstallDictionary(const QString& locale);
|
||||
|
||||
Q_INVOKABLE QVariantList spellSuggestionsRequest(const QString& word);
|
||||
Q_INVOKABLE bool spell(const QString& word);
|
||||
Q_INVOKABLE QVariantList findWords(const QString& text);
|
||||
|
||||
public Q_SLOTS:
|
||||
Q_INVOKABLE void setDictionary(const QString& locale);
|
||||
|
||||
Q_SIGNALS:
|
||||
void dictionaryChanged();
|
||||
void downloadFailed(const QString& locale);
|
||||
|
||||
private:
|
||||
SpellChecker spellChecker_;
|
||||
SpellCheckDictionaryListModel* dictionaryListModel_ {nullptr};
|
||||
AppSettingsManager* settingsManager_ {nullptr};
|
||||
};
|
||||
460
src/app/spellcheckdictionarylistmodel.cpp
Normal file
460
src/app/spellcheckdictionarylistmodel.cpp
Normal file
@ -0,0 +1,460 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Savoir-faire Linux Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "spellcheckdictionarylistmodel.h"
|
||||
|
||||
#include "global.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QBuffer>
|
||||
#include <QClipboard>
|
||||
#include <QFileInfo>
|
||||
#include <QRegExp>
|
||||
#include <QMimeData>
|
||||
#include <QDir>
|
||||
#include <QMimeDatabase>
|
||||
#include <QUrl>
|
||||
#include <QRegularExpression>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QStandardPaths>
|
||||
#include <QFile>
|
||||
|
||||
SpellCheckDictionaryListModel::SpellCheckDictionaryListModel(AppSettingsManager* settingsManager,
|
||||
ConnectivityMonitor* cm,
|
||||
QObject* parent)
|
||||
: QAbstractListModel(parent)
|
||||
, spellCheckFileDownloader_(new FileDownloader(cm, this))
|
||||
, settingsManager_(settingsManager)
|
||||
{
|
||||
// Connect FileDownloader signals
|
||||
connect(spellCheckFileDownloader_,
|
||||
&FileDownloader::downloadFileSuccessful,
|
||||
this,
|
||||
&SpellCheckDictionaryListModel::onDownloadFileFinished);
|
||||
connect(spellCheckFileDownloader_,
|
||||
&FileDownloader::downloadFileFailed,
|
||||
this,
|
||||
&SpellCheckDictionaryListModel::onDownloadFileFailed);
|
||||
|
||||
// Initialize the model with available dictionaries and check if dictionaries are available
|
||||
// This will determine whether we need to notify the UI about a new available dictionary,
|
||||
// which is important because we want SpellCheckAdapter to be able to set the dictionary path
|
||||
// but only when dictionaries are available after initialization, and not on every download.
|
||||
dictionariesAvailable_ = populateDictionaries() > 0;
|
||||
|
||||
// First, correct/migrate a bad setting that may have been set in the past
|
||||
auto spellLangLocale = settingsManager_->getValue(Settings::Key::SpellLang).toString();
|
||||
auto currentLocale = settingsManager_->getLanguage();
|
||||
if (spellLangLocale.isEmpty() || !isLocaleInstalled(spellLangLocale)) {
|
||||
C_WARN << "Spell check language setting is empty or invalid, resetting to current locale";
|
||||
settingsManager_->setValue(Settings::Key::SpellLang, currentLocale);
|
||||
installDictionary(currentLocale);
|
||||
}
|
||||
}
|
||||
|
||||
QString
|
||||
SpellCheckDictionaryListModel::pathForLocale(const QString& locale) const
|
||||
{
|
||||
// Find the dictionary in the model and construct the path based on the locale and the
|
||||
// dictionary type (Jami-install or system)
|
||||
const auto index = getDictionaryIndex(locale);
|
||||
if (!index.isValid()) {
|
||||
return {};
|
||||
}
|
||||
const auto& dictObj = dictionaries_.at(index.row()).toObject();
|
||||
if (dictObj.value("isSystem").toBool()) {
|
||||
return systemDictionariesPath_ + locale;
|
||||
}
|
||||
return dictionariesPath_ + locale;
|
||||
}
|
||||
|
||||
int
|
||||
SpellCheckDictionaryListModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
return dictionaries_.size();
|
||||
}
|
||||
|
||||
QVariant
|
||||
SpellCheckDictionaryListModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return {};
|
||||
// Try to find the item at the index
|
||||
const auto& item = dictionaries_.at(index.row());
|
||||
if (!item.isObject())
|
||||
return {};
|
||||
// Try to convert the item to a QJsonObject
|
||||
const auto& itemObject = item.toObject().toVariantMap();
|
||||
switch (role) {
|
||||
case Role::NativeName:
|
||||
return itemObject.value("nativeName");
|
||||
case Role::Path:
|
||||
return itemObject.value("path");
|
||||
case Role::Locale:
|
||||
return itemObject.value("locale");
|
||||
case Role::Installed:
|
||||
return itemObject.value("installed").toBool();
|
||||
case Role::Downloading:
|
||||
return pendingDownloads_.contains(itemObject.value("locale").toString());
|
||||
case Role::IsSystem:
|
||||
return itemObject.value("isSystem").toBool();
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray>
|
||||
SpellCheckDictionaryListModel::roleNames() const
|
||||
{
|
||||
using namespace SpellCheckDictionaryList;
|
||||
QHash<int, QByteArray> roles;
|
||||
#define X(role) roles[role] = #role;
|
||||
SPELL_CHECK_DICTIONARY_MODEL_ROLES
|
||||
#undef X
|
||||
return roles;
|
||||
}
|
||||
|
||||
int
|
||||
SpellCheckDictionaryListModel::populateDictionaries()
|
||||
{
|
||||
// First, we need to get the list of available dictionaries.
|
||||
QFile availableDictionariesFile(":/misc/available_dictionaries.json");
|
||||
if (!availableDictionariesFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
C_WARN << "Available Dictionaries file failed to load";
|
||||
return 0;
|
||||
}
|
||||
const auto availableDictionaries = QString(availableDictionariesFile.readAll());
|
||||
QJsonDocument doc = QJsonDocument::fromJson(availableDictionaries.toUtf8());
|
||||
/*
|
||||
The file is a JSON object with the following structure:
|
||||
{
|
||||
"af_ZA": {
|
||||
"nativeName": "Afrikaans (Suid-Afrika)",
|
||||
"path": "af_ZA/af_ZA"
|
||||
},
|
||||
...
|
||||
}
|
||||
We want to convert it to a QJsonArray of QJsonObjects, each containing the locale,
|
||||
nativeName, path, and installed status.
|
||||
*/
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
C_WARN.noquote() << "Available Dictionaries file is not a valid JSON object";
|
||||
return 0;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
dictionaries_ = QJsonArray();
|
||||
|
||||
const auto object = doc.object();
|
||||
|
||||
// Get installed dictionaries to check status
|
||||
QString hunspellDataDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
|
||||
+ "/dictionaries/";
|
||||
QDir dictionariesDir(hunspellDataDir);
|
||||
QRegExp regex("(.*).dic");
|
||||
QStringList installedLocales;
|
||||
|
||||
// Check for dictionary files in the base directory
|
||||
QStringList rootDicFiles = dictionariesDir.entryList(QStringList() << "*.dic", QDir::Files);
|
||||
for (const auto& dicFile : rootDicFiles) {
|
||||
regex.indexIn(dicFile);
|
||||
auto captured = regex.capturedTexts();
|
||||
if (captured.size() == 2) {
|
||||
installedLocales << captured[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Check for dictionary files in the system directory
|
||||
QStringList systemDicFiles = QDir(systemDictionariesPath_)
|
||||
.entryList(QStringList() << "*.dic", QDir::Files);
|
||||
for (const auto& dicFile : systemDicFiles) {
|
||||
regex.indexIn(dicFile);
|
||||
auto captured = regex.capturedTexts();
|
||||
if (captured.size() == 2) {
|
||||
installedLocales << captured[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Add the dictionaries to the model with the appropriate flags
|
||||
for (const auto& key : object.keys()) {
|
||||
const auto valueObject = object.value(key).toObject();
|
||||
bool isSystem = systemDicFiles.contains(key + ".dic");
|
||||
bool isInstalled = installedLocales.contains(key);
|
||||
dictionaries_.append(QJsonObject {{"locale", key},
|
||||
{"nativeName", valueObject.value("nativeName").toString()},
|
||||
{"path", valueObject.value("path").toString()},
|
||||
{"installed", isInstalled},
|
||||
{"isSystem", isSystem}});
|
||||
}
|
||||
endResetModel();
|
||||
return installedLocales.size();
|
||||
}
|
||||
|
||||
Q_INVOKABLE void
|
||||
SpellCheckDictionaryListModel::installDictionary(const QString& locale)
|
||||
{
|
||||
// Check if dictionary is already installed
|
||||
const auto index = getDictionaryIndex(locale);
|
||||
if (!index.isValid()) {
|
||||
C_WARN << "Dictionary not found for locale:" << locale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already installed
|
||||
auto dictObj = dictionaries_.at(index.row()).toObject();
|
||||
if (dictObj.value("installed").toBool()) {
|
||||
C_WARN << "Dictionary already installed for locale:" << locale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if download is already in progress
|
||||
if (pendingDownloads_.contains(locale)) {
|
||||
C_WARN << "Download already in progress for locale:" << locale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the download process
|
||||
downloadDictionaryFiles(locale);
|
||||
}
|
||||
|
||||
Q_INVOKABLE void
|
||||
SpellCheckDictionaryListModel::uninstallDictionary(const QString& locale)
|
||||
{
|
||||
const auto index = getDictionaryIndex(locale);
|
||||
if (!index.isValid()) {
|
||||
C_WARN << "Dictionary not found for locale:" << locale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if dictionary is actually installed
|
||||
auto dictObj = dictionaries_.at(index.row()).toObject();
|
||||
if (!dictObj.value("installed").toBool()) {
|
||||
C_WARN << "Dictionary not installed for locale:" << locale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the dictionary is a system dictionary (cannot be uninstalled here)
|
||||
if (dictObj.value("isSystem").toBool()) {
|
||||
C_WARN << "Cannot uninstall system dictionary:" << locale;
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete the dictionary files
|
||||
QString affFile = dictionariesPath_ + locale + ".aff";
|
||||
QString dicFile = dictionariesPath_ + locale + ".dic";
|
||||
|
||||
bool affDeleted = true;
|
||||
bool dicDeleted = true;
|
||||
|
||||
if (QFile::exists(affFile)) {
|
||||
affDeleted = QFile::remove(affFile);
|
||||
if (!affDeleted) {
|
||||
C_WARN << "Failed to delete .aff file:" << affFile;
|
||||
}
|
||||
}
|
||||
|
||||
if (QFile::exists(dicFile)) {
|
||||
dicDeleted = QFile::remove(dicFile);
|
||||
if (!dicDeleted) {
|
||||
C_WARN << "Failed to delete .dic file:" << dicFile;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the installation status regardless of file deletion success
|
||||
// Note: This ensures the UI reflects the uninstall attempt only
|
||||
updateDictionaryInstallationStatus(locale, false);
|
||||
|
||||
if (affDeleted && dicDeleted) {
|
||||
C_DBG << "Dictionary uninstalled successfully for locale:" << locale;
|
||||
Q_EMIT uninstallFinished(locale);
|
||||
} else {
|
||||
C_WARN << "Dictionary uninstall completed with errors for locale:" << locale;
|
||||
Q_EMIT uninstallFailed(locale);
|
||||
}
|
||||
}
|
||||
|
||||
QVariantMap
|
||||
SpellCheckDictionaryListModel::getInstalledDictionaries() const
|
||||
{
|
||||
QVariantMap installedDictionaries;
|
||||
for (const auto& dict : dictionaries_) {
|
||||
const auto dictObj = dict.toObject();
|
||||
if (dictObj.value("installed").toBool()) {
|
||||
installedDictionaries.insert(dictObj.value("locale").toString(),
|
||||
dictObj.value("nativeName").toString());
|
||||
}
|
||||
}
|
||||
return installedDictionaries;
|
||||
}
|
||||
|
||||
QModelIndex
|
||||
SpellCheckDictionaryListModel::getDictionaryIndex(const QString& locale) const
|
||||
{
|
||||
for (int i = 0; i < dictionaries_.size(); ++i) {
|
||||
if (dictionaries_.at(i).toObject().value("locale") == locale)
|
||||
return createIndex(i, 0);
|
||||
}
|
||||
return {}; // Not found
|
||||
}
|
||||
|
||||
bool
|
||||
SpellCheckDictionaryListModel::isLocaleInstalled(const QString& locale) const
|
||||
{
|
||||
// Iterate through the dictionaries to check if the locale is installed
|
||||
for (const auto& dict : dictionaries_) {
|
||||
if (dict.toObject().value("locale").toString() == locale) {
|
||||
return dict.toObject().value("installed").toBool();
|
||||
}
|
||||
}
|
||||
return false; // Locale not found
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckDictionaryListModel::downloadDictionaryFiles(const QString& locale)
|
||||
{
|
||||
C_DBG << "Downloading dictionary:" << locale;
|
||||
|
||||
// Find the dictionary info
|
||||
const auto index = getDictionaryIndex(locale);
|
||||
if (!index.isValid()) {
|
||||
C_WARN << "Cannot download: dictionary not found for locale:" << locale;
|
||||
Q_EMIT downloadFailed(locale);
|
||||
return;
|
||||
}
|
||||
|
||||
auto dictObj = dictionaries_.at(index.row()).toObject();
|
||||
QString basePath = dictObj.value("path").toString();
|
||||
|
||||
if (basePath.isEmpty()) {
|
||||
C_WARN << "Cannot download: invalid path for dictionary" << locale;
|
||||
Q_EMIT downloadFailed(locale);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to pending downloads
|
||||
pendingDownloads_.append(locale);
|
||||
Q_EMIT dataChanged(index, index, {Role::Downloading});
|
||||
|
||||
// Create target directory if it doesn't exist
|
||||
QDir().mkpath(dictionariesPath_);
|
||||
|
||||
QString targetFile = dictionariesPath_ + locale;
|
||||
|
||||
// Construct URLs using the stored path
|
||||
QString baseUrl = downloadUrl_.toString();
|
||||
QUrl urlAff = baseUrl + "/" + basePath + ".aff";
|
||||
QUrl urlDic = baseUrl + "/" + basePath + ".dic";
|
||||
|
||||
C_DBG << "Downloading dictionary files for" << locale;
|
||||
|
||||
// Start downloads
|
||||
spellCheckFileDownloader_->downloadFile(urlAff, targetFile + ".aff");
|
||||
spellCheckFileDownloader_->downloadFile(urlDic, targetFile + ".dic");
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckDictionaryListModel::updateDictionaryInstallationStatus(const QString& locale,
|
||||
bool installed)
|
||||
{
|
||||
const auto index = getDictionaryIndex(locale);
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the dictionary object
|
||||
auto dictObj = dictionaries_.at(index.row()).toObject();
|
||||
dictObj["installed"] = installed;
|
||||
dictionaries_[index.row()] = dictObj;
|
||||
|
||||
// Emit data changed signal
|
||||
Q_EMIT dataChanged(index, index, {SpellCheckDictionaryList::Installed});
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckDictionaryListModel::notifyDownloadStateChanged(const QString& locale)
|
||||
{
|
||||
auto index = getDictionaryIndex(locale);
|
||||
if (index.isValid()) {
|
||||
Q_EMIT dataChanged(index, index, {Role::Downloading});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckDictionaryListModel::onDownloadFileFinished(const QString& localPath)
|
||||
{
|
||||
C_DBG << "Download finished:" << localPath;
|
||||
|
||||
// Extract locale from file path
|
||||
QFileInfo fileInfo(localPath);
|
||||
QString locale = fileInfo.baseName();
|
||||
|
||||
static auto handleDownloadComplete = [this, &locale](const QString& localPath) {
|
||||
// Both files are now available, mark as installed
|
||||
updateDictionaryInstallationStatus(locale, true);
|
||||
pendingDownloads_.removeAll(locale);
|
||||
notifyDownloadStateChanged(locale);
|
||||
Q_EMIT downloadFinished(locale);
|
||||
C_DBG << "Dictionary installation completed for:" << locale;
|
||||
|
||||
// If we want to prevent installed dictionaries from being automatically set as the current
|
||||
// dictionary, then place the newDictionaryAvailable signal within the if block.
|
||||
Q_EMIT newDictionaryAvailable(locale);
|
||||
if (!dictionariesAvailable_) {
|
||||
dictionariesAvailable_ = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Check if this is a .dic file and if the corresponding .aff file exists
|
||||
if (localPath.endsWith(".dic")) {
|
||||
QString affFilePath = localPath;
|
||||
affFilePath.chop(4); // Remove ".dic"
|
||||
affFilePath += ".aff";
|
||||
|
||||
if (QFile::exists(affFilePath)) {
|
||||
handleDownloadComplete(affFilePath);
|
||||
} else {
|
||||
C_DBG << "Waiting for .aff file for:" << locale;
|
||||
}
|
||||
} else if (localPath.endsWith(".aff")) {
|
||||
QString dicFilePath = localPath;
|
||||
dicFilePath.chop(4); // Remove ".aff"
|
||||
dicFilePath += ".dic";
|
||||
|
||||
if (QFile::exists(dicFilePath)) {
|
||||
handleDownloadComplete(dicFilePath);
|
||||
} else {
|
||||
C_DBG << "Waiting for .dic file for:" << locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SpellCheckDictionaryListModel::onDownloadFileFailed(const QString& localPath)
|
||||
{
|
||||
C_WARN << "Download failed for file:" << localPath;
|
||||
|
||||
// Extract locale from file path
|
||||
QFileInfo fileInfo(localPath);
|
||||
QString locale = fileInfo.baseName();
|
||||
|
||||
// Remove from pending downloads and emit failure signal
|
||||
pendingDownloads_.removeAll(locale);
|
||||
notifyDownloadStateChanged(locale);
|
||||
Q_EMIT downloadFailed(locale);
|
||||
}
|
||||
128
src/app/spellcheckdictionarylistmodel.h
Normal file
128
src/app/spellcheckdictionarylistmodel.h
Normal file
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Savoir-faire Linux Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "appsettingsmanager.h"
|
||||
#include "connectivitymonitor.h"
|
||||
#include "filedownloader.h"
|
||||
#include "systemtray.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QApplication>
|
||||
#include <QQmlEngine>
|
||||
#include <QUrl>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QtCore/QLoggingCategory>
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#define SPELL_CHECK_DICTIONARY_MODEL_ROLES \
|
||||
X(NativeName) \
|
||||
X(Path) \
|
||||
X(Locale) \
|
||||
X(Installed) \
|
||||
X(Downloading) \
|
||||
X(IsSystem)
|
||||
|
||||
namespace SpellCheckDictionaryList {
|
||||
Q_NAMESPACE
|
||||
enum Role {
|
||||
DummyRole = Qt::UserRole + 1,
|
||||
#define X(role) role,
|
||||
SPELL_CHECK_DICTIONARY_MODEL_ROLES
|
||||
#undef X
|
||||
};
|
||||
Q_ENUM_NS(Role)
|
||||
} // namespace SpellCheckDictionaryList
|
||||
|
||||
class SpellCheckDictionaryListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SpellCheckDictionaryListModel(AppSettingsManager* settingsManager,
|
||||
ConnectivityMonitor* cm,
|
||||
QObject* parent = nullptr);
|
||||
~SpellCheckDictionaryListModel() override = default;
|
||||
|
||||
// Construct the final path for a given locale. This could be either
|
||||
// a Jami-install or a system dictionary.
|
||||
QString pathForLocale(const QString& locale) const;
|
||||
|
||||
// QAbstractListModel interface
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
// SpellCheckAdapter needs access to the dictionary management methods
|
||||
friend class SpellCheckAdapter;
|
||||
|
||||
Q_SIGNALS:
|
||||
void downloadFinished(const QString& locale);
|
||||
void downloadFailed(const QString& locale);
|
||||
void uninstallFinished(const QString& locale);
|
||||
void uninstallFailed(const QString& locale);
|
||||
|
||||
// When a new dictionary is available, emit the signal so we can use
|
||||
// it to set the current dictionary from the SpellCheckAdapter
|
||||
void newDictionaryAvailable(const QString& locale);
|
||||
|
||||
protected:
|
||||
using Role = SpellCheckDictionaryList::Role;
|
||||
|
||||
void installDictionary(const QString& locale);
|
||||
void uninstallDictionary(const QString& locale);
|
||||
|
||||
QVariantMap getInstalledDictionaries() const;
|
||||
|
||||
private Q_SLOTS:
|
||||
void onDownloadFileFinished(const QString& localPath);
|
||||
void onDownloadFileFailed(const QString& localPath);
|
||||
|
||||
private:
|
||||
QJsonArray dictionaries_; // Principal underlying data structure
|
||||
QStringList pendingDownloads_; // Used to track pending downloads and status
|
||||
bool dictionariesAvailable_ {false}; // Flag to indicate if dictionaries are available
|
||||
|
||||
int populateDictionaries(); // Returns number of installed dictionaries
|
||||
|
||||
// Utility to get the index of a dictionary
|
||||
QModelIndex getDictionaryIndex(const QString& locale) const;
|
||||
bool isLocaleInstalled(const QString& locale) const;
|
||||
|
||||
// Helper methods for download functionality
|
||||
void downloadDictionaryFiles(const QString& locale);
|
||||
void updateDictionaryInstallationStatus(const QString& locale, bool installed);
|
||||
void notifyDownloadStateChanged(const QString& locale);
|
||||
|
||||
// Dictionary file management and downloading
|
||||
FileDownloader* spellCheckFileDownloader_;
|
||||
const QUrl downloadUrl_ {"https://raw.githubusercontent.com/LibreOffice/dictionaries/master"};
|
||||
const QString dictionariesPath_ = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
|
||||
+ "/dictionaries/";
|
||||
#if defined(Q_OS_LINUX)
|
||||
const QString systemDictionariesPath_ = "/usr/share/hunspell/";
|
||||
#elif defined(Q_OS_MACOS)
|
||||
const QString systemDictionariesPath_ = "/Library/Spelling/";
|
||||
#else
|
||||
const QString systemDictionariesPath_ = "";
|
||||
#endif
|
||||
|
||||
AppSettingsManager* settingsManager_;
|
||||
};
|
||||
@ -1,149 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Savoir-faire Linux Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "spellcheckdictionarymanager.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QBuffer>
|
||||
#include <QClipboard>
|
||||
#include <QFileInfo>
|
||||
#include <QRegExp>
|
||||
#include <QMimeData>
|
||||
#include <QDir>
|
||||
#include <QMimeDatabase>
|
||||
#include <QUrl>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QEventLoop>
|
||||
#include <QRegularExpression>
|
||||
|
||||
SpellCheckDictionaryManager::SpellCheckDictionaryManager(AppSettingsManager* settingsManager,
|
||||
QObject* parent)
|
||||
: QObject {parent}
|
||||
, settingsManager_ {settingsManager}
|
||||
{}
|
||||
|
||||
QVariantMap
|
||||
SpellCheckDictionaryManager::installedDictionaries()
|
||||
{
|
||||
// If we already have a cache of the installed dictionaries, return it
|
||||
if (cachedInstalledDictionaries_.size() > 0) {
|
||||
return cachedInstalledDictionaries_;
|
||||
|
||||
// If not, we need to check the dictionaries directory
|
||||
} else {
|
||||
QString hunspellDataDir = getDictionariesPath();
|
||||
|
||||
auto dictionariesDir = QDir(hunspellDataDir);
|
||||
QRegExp regex("(.*).dic");
|
||||
QSet<QString> nativeNames;
|
||||
|
||||
QVariantMap result;
|
||||
result["NONE"] = tr("None");
|
||||
QStringList folders = dictionariesDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
// Check for dictionary files in the base directory
|
||||
QStringList rootDicFiles = dictionariesDir.entryList(QStringList() << "*.dic", QDir::Files);
|
||||
for (const auto& dicFile : rootDicFiles) {
|
||||
regex.indexIn(dicFile);
|
||||
auto captured = regex.capturedTexts();
|
||||
if (captured.size() == 2) {
|
||||
auto nativeName = QLocale(captured[1]).nativeLanguageName();
|
||||
if (!nativeName.isEmpty() && !nativeNames.contains(nativeName)) {
|
||||
result[captured[1]] = nativeName;
|
||||
nativeNames.insert(nativeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for dictionary files in subdirectories
|
||||
for (const auto& folder : folders) {
|
||||
QDir subDir = dictionariesDir.absoluteFilePath(folder);
|
||||
QStringList dicFiles = subDir.entryList(QStringList() << "*.dic", QDir::Files);
|
||||
subDir.setFilter(QDir::Files | QDir::AllDirs | QDir::NoDotAndDotDot);
|
||||
subDir.setSorting(QDir::DirsFirst);
|
||||
QFileInfoList list = subDir.entryInfoList();
|
||||
for (const auto& fileInfo : list) {
|
||||
if (fileInfo.isDir()) {
|
||||
QDir recursiveDir(fileInfo.absoluteFilePath());
|
||||
QStringList recursiveDicFiles = recursiveDir.entryList(QStringList() << "*.dic",
|
||||
QDir::Files);
|
||||
if (!recursiveDicFiles.isEmpty()) {
|
||||
dicFiles.append(recursiveDicFiles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the locale from the dictionary file names
|
||||
for (const auto& dicFile : dicFiles) {
|
||||
regex.indexIn(dicFile);
|
||||
auto captured = regex.capturedTexts();
|
||||
|
||||
if (captured.size() == 2) {
|
||||
auto nativeName = QLocale(captured[1]).nativeLanguageName();
|
||||
|
||||
if (nativeName.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!nativeNames.contains(nativeName)) {
|
||||
result[folder + QDir::separator() + captured[1]] = nativeName;
|
||||
nativeNames.insert(nativeName);
|
||||
} else {
|
||||
qWarning() << "Duplicate native name found, skipping:" << nativeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cachedInstalledDictionaries_ = result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
QString
|
||||
SpellCheckDictionaryManager::getDictionariesPath()
|
||||
{
|
||||
#if defined(Q_OS_LINUX)
|
||||
QString hunDir = "/usr/share/hunspell/";
|
||||
;
|
||||
|
||||
#elif defined(Q_OS_MACOS)
|
||||
QString hunDir = "/Library/Spelling/";
|
||||
#else
|
||||
QString hunDir = "";
|
||||
#endif
|
||||
return hunDir;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SpellCheckDictionaryManager::refreshDictionaries()
|
||||
{
|
||||
cachedInstalledDictionaries_.clear();
|
||||
}
|
||||
|
||||
QString
|
||||
SpellCheckDictionaryManager::getSpellLanguage()
|
||||
{
|
||||
auto pref = settingsManager_->getValue(Settings::Key::SpellLang).toString();
|
||||
return pref ;
|
||||
}
|
||||
|
||||
// Is only used at application boot time
|
||||
QString
|
||||
SpellCheckDictionaryManager::getDictionaryPath()
|
||||
{
|
||||
return "/usr/share/hunspell/" + getSpellLanguage();
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Savoir-faire Linux Inc.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "appsettingsmanager.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QApplication>
|
||||
#include <QQmlEngine>
|
||||
|
||||
class SpellCheckDictionaryManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QVariantMap cachedInstalledDictionaries_;
|
||||
AppSettingsManager* settingsManager_;
|
||||
public:
|
||||
explicit SpellCheckDictionaryManager(AppSettingsManager* settingsManager,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
Q_INVOKABLE QVariantMap installedDictionaries();
|
||||
Q_INVOKABLE QString getDictionariesPath();
|
||||
Q_INVOKABLE void refreshDictionaries();
|
||||
Q_INVOKABLE QString getDictionaryPath();
|
||||
Q_INVOKABLE QString getSpellLanguage();
|
||||
};
|
||||
@ -29,27 +29,30 @@
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatchIterator>
|
||||
|
||||
SpellChecker::SpellChecker(const QString& dictionaryPath)
|
||||
SpellChecker::SpellChecker()
|
||||
: hunspell_(new Hunspell("", ""))
|
||||
{
|
||||
replaceDictionary(dictionaryPath);
|
||||
// Initialize with default UTF-8 codec
|
||||
codec_ = QTextCodec::codecForName("UTF-8");
|
||||
}
|
||||
|
||||
bool
|
||||
SpellChecker::spell(const QString& word)
|
||||
{
|
||||
// Encode from Unicode to the encoding used by current dictionary
|
||||
return hunspell_->spell(word.toStdString()) != 0;
|
||||
return hunspell_->spell(codec_->fromUnicode(word).toStdString()) != 0;
|
||||
}
|
||||
|
||||
QStringList
|
||||
SpellChecker::suggest(const QString& word)
|
||||
{
|
||||
// Encode from Unicode to the encoding used by current dictionary
|
||||
std::vector<std::string> numSuggestions = hunspell_->suggest(word.toStdString());
|
||||
std::vector<std::string> numSuggestions = hunspell_->suggest(
|
||||
codec_->fromUnicode(word).constData());
|
||||
QStringList suggestions;
|
||||
|
||||
for (size_t i = 0; i < numSuggestions.size(); ++i) {
|
||||
suggestions << QString::fromStdString(numSuggestions.at(i));
|
||||
for (const auto& suggestion : numSuggestions) {
|
||||
suggestions << codec_->toUnicode(suggestion.c_str());
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
@ -66,34 +69,37 @@ SpellChecker::put_word(const QString& word)
|
||||
hunspell_->add(codec_->fromUnicode(word).constData());
|
||||
}
|
||||
|
||||
void
|
||||
bool
|
||||
SpellChecker::replaceDictionary(const QString& dictionaryPath)
|
||||
{
|
||||
if (dictionaryPath == currentDictionaryPath_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString dictFile = dictionaryPath + ".dic";
|
||||
QString affixFile = dictionaryPath + ".aff";
|
||||
QByteArray dictFilePathBA = dictFile.toLocal8Bit();
|
||||
QByteArray affixFilePathBA = affixFile.toLocal8Bit();
|
||||
if (hunspell_) {
|
||||
hunspell_.reset();
|
||||
}
|
||||
hunspell_ = std::make_shared<Hunspell>(affixFilePathBA.constData(), dictFilePathBA.constData());
|
||||
|
||||
// detect encoding analyzing the SET option in the affix file
|
||||
encoding_ = "ISO8859-1";
|
||||
QFile _affixFile(affixFile);
|
||||
if (_affixFile.open(QIODevice::ReadOnly)) {
|
||||
QTextStream stream(&_affixFile);
|
||||
QRegExp enc_detector("^\\s*SET\\s+([A-Z0-9\\-]+)\\s*", Qt::CaseInsensitive);
|
||||
for (QString line = stream.readLine(); !line.isEmpty(); line = stream.readLine()) {
|
||||
if (enc_detector.indexIn(line) > -1) {
|
||||
encoding_ = enc_detector.cap(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_affixFile.close();
|
||||
// Check if dictionary files exist
|
||||
if (!QFile::exists(dictFile) || !QFile::exists(affixFile)) {
|
||||
qWarning() << "Dictionary files not found:" << dictFile << affixFile;
|
||||
return false;
|
||||
}
|
||||
|
||||
codec_ = QTextCodec::codecForName(this->encoding_.toLatin1().constData());
|
||||
QByteArray dictFilePath = dictFile.toLocal8Bit();
|
||||
QByteArray affixFilePath = affixFile.toLocal8Bit();
|
||||
|
||||
std::unique_ptr<Hunspell> hunspell(
|
||||
new Hunspell(affixFilePath.constData(), dictFilePath.constData()));
|
||||
auto encoding = hunspell->get_dic_encoding();
|
||||
auto codec = QTextCodec::codecForName(encoding);
|
||||
if (!codec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hunspell_ = std::move(hunspell);
|
||||
codec_ = codec;
|
||||
currentDictionaryPath_ = dictionaryPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<SpellChecker::WordInfo>
|
||||
@ -101,7 +107,7 @@ SpellChecker::findWords(const QString& text)
|
||||
{
|
||||
// This is in the C++ part of the code because QML regex does not support unicode
|
||||
QList<WordInfo> results;
|
||||
QRegularExpression regex("\\p{L}+|\\p{N}+");
|
||||
QRegularExpression regex("\\p{L}+");
|
||||
QRegularExpressionMatchIterator iter = regex.globalMatch(text);
|
||||
|
||||
while (iter.hasNext()) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user