spectral/0002755000175000000620000000000013570507025012356 5ustar dilingerstaffspectral/cmake/0002755000175000000620000000000013566674120013444 5ustar dilingerstaffspectral/cmake/ECMInstallIcons.cmake0000644000175000000620000003047713566674120017406 0ustar dilingerstaff#.rst: # ECMInstallIcons # --------------- # # Installs icons, sorting them into the correct directories according to the # FreeDesktop.org icon naming specification. # # :: # # ecm_install_icons(ICONS [ [...]] # DESTINATION # [LANG ] # [THEME ]) # # The given icons, whose names must match the pattern:: # # --. # # will be installed to the appropriate subdirectory of DESTINATION according to # the FreeDesktop.org icon naming scheme. By default, they are installed to the # "hicolor" theme, but this can be changed using the THEME argument. If the # icons are localized, the LANG argument can be used to install them in a # locale-specific directory. # # ```` is a numeric pixel size (typically 16, 22, 32, 48, 64, 128 or 256) # or ``sc`` for scalable (SVG) files, ```` is one of the standard # FreeDesktop.org icon groups (actions, animations, apps, categories, devices, # emblems, emotes, intl, mimetypes, places, status) and ```` is one of # ``.png``, ``.mng`` or ``.svgz``. # # The typical installation directory is ``share/icons``. # # .. code-block:: cmake # # ecm_install_icons(ICONS 22-actions-menu_new.png # DESTINATION share/icons) # # The above code will install the file ``22-actions-menu_new.png`` as # ``${CMAKE_INSTALL_PREFIX}/share/icons//22x22/actions/menu_new.png`` # # Users of the :kde-module:`KDEInstallDirs` module would normally use # ``${ICON_INSTALL_DIR}`` as the DESTINATION, while users of the GNUInstallDirs # module should use ``${CMAKE_INSTALL_DATAROOTDIR}/icons``. # # An old form of arguments will also be accepted:: # # ecm_install_icons( []) # # This matches files named like:: # # --. # # where ```` is one of # * ``hi`` for hicolor # * ``lo`` for locolor # * ``cr`` for the Crystal icon theme # * ``ox`` for the Oxygen icon theme # * ``br`` for the Breeze icon theme # # With this syntax, the file ``hi22-actions-menu_new.png`` would be installed # into ``/hicolor/22x22/actions/menu_new.png`` # # Since pre-1.0.0. #============================================================================= # Copyright 2014 Alex Merry # Copyright 2013 David Edmundson # Copyright 2008 Chusslove Illich # Copyright 2006 Alex Neundorf # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. include(CMakeParseArguments) # A "map" of short type names to the directories. # Unknown names produce a warning. set(_ECM_ICON_GROUP_mimetypes "mimetypes") set(_ECM_ICON_GROUP_places "places") set(_ECM_ICON_GROUP_devices "devices") set(_ECM_ICON_GROUP_apps "apps") set(_ECM_ICON_GROUP_actions "actions") set(_ECM_ICON_GROUP_categories "categories") set(_ECM_ICON_GROUP_status "status") set(_ECM_ICON_GROUP_emblems "emblems") set(_ECM_ICON_GROUP_emotes "emotes") set(_ECM_ICON_GROUP_animations "animations") set(_ECM_ICON_GROUP_intl "intl") # For the "compatibility" syntax: a "map" of short theme names to the theme # directory set(_ECM_ICON_THEME_br "breeze") set(_ECM_ICON_THEME_ox "oxygen") set(_ECM_ICON_THEME_cr "crystalsvg") set(_ECM_ICON_THEME_lo "locolor") set(_ECM_ICON_THEME_hi "hicolor") macro(_ecm_install_icons_v1 _defaultpath) # the l10n-subdir if language given as second argument (localized icon) set(_lang ${ARGV1}) if(_lang) set(_l10n_SUBDIR l10n/${_lang}) else() set(_l10n_SUBDIR ".") endif() set(_themes) # first the png icons file(GLOB _icons *.png) foreach (_current_ICON ${_icons} ) # since CMake 2.6 regex matches are stored in special variables CMAKE_MATCH_x, if it didn't match, they are empty string(REGEX MATCH "^.*/([a-zA-Z]+)([0-9]+)\\-([a-z]+)\\-(.+\\.png)$" _dummy "${_current_ICON}") set(_type "${CMAKE_MATCH_1}") set(_size "${CMAKE_MATCH_2}") set(_group "${CMAKE_MATCH_3}") set(_name "${CMAKE_MATCH_4}") set(_theme_GROUP ${_ECM_ICON_THEME_${_type}}) if( _theme_GROUP) list(APPEND _themes "${_theme_GROUP}") _ECM_ADD_ICON_INSTALL_RULE(${CMAKE_CURRENT_BINARY_DIR}/install_icons.cmake ${_defaultpath}/${_theme_GROUP}/${_size}x${_size} ${_group} ${_current_ICON} ${_name} ${_l10n_SUBDIR}) endif() endforeach (_current_ICON) # mng icons file(GLOB _icons *.mng) foreach (_current_ICON ${_icons} ) # since CMake 2.6 regex matches are stored in special variables CMAKE_MATCH_x, if it didn't match, they are empty string(REGEX MATCH "^.*/([a-zA-Z]+)([0-9]+)\\-([a-z]+)\\-(.+\\.mng)$" _dummy "${_current_ICON}") set(_type "${CMAKE_MATCH_1}") set(_size "${CMAKE_MATCH_2}") set(_group "${CMAKE_MATCH_3}") set(_name "${CMAKE_MATCH_4}") set(_theme_GROUP ${_ECM_ICON_THEME_${_type}}) if( _theme_GROUP) list(APPEND _themes "${_theme_GROUP}") _ECM_ADD_ICON_INSTALL_RULE(${CMAKE_CURRENT_BINARY_DIR}/install_icons.cmake ${_defaultpath}/${_theme_GROUP}/${_size}x${_size} ${_group} ${_current_ICON} ${_name} ${_l10n_SUBDIR}) endif() endforeach (_current_ICON) # and now the svg icons file(GLOB _icons *.svgz) foreach (_current_ICON ${_icons} ) # since CMake 2.6 regex matches are stored in special variables CMAKE_MATCH_x, if it didn't match, they are empty string(REGEX MATCH "^.*/([a-zA-Z]+)sc\\-([a-z]+)\\-(.+\\.svgz)$" _dummy "${_current_ICON}") set(_type "${CMAKE_MATCH_1}") set(_group "${CMAKE_MATCH_2}") set(_name "${CMAKE_MATCH_3}") set(_theme_GROUP ${_ECM_ICON_THEME_${_type}}) if( _theme_GROUP) list(APPEND _themes "${_theme_GROUP}") _ECM_ADD_ICON_INSTALL_RULE(${CMAKE_CURRENT_BINARY_DIR}/install_icons.cmake ${_defaultpath}/${_theme_GROUP}/scalable ${_group} ${_current_ICON} ${_name} ${_l10n_SUBDIR}) endif() endforeach (_current_ICON) if (_themes) list(REMOVE_DUPLICATES _themes) foreach(_theme ${_themes}) _ecm_update_iconcache("${_defaultpath}" "${_theme}") endforeach() else() message(AUTHOR_WARNING "No suitably-named icons found") endif() endmacro() # only used internally by _ecm_install_icons_v1 macro(_ecm_add_icon_install_rule _install_SCRIPT _install_PATH _group _orig_NAME _install_NAME _l10n_SUBDIR) # if the string doesn't match the pattern, the result is the full string, so all three have the same content if (NOT ${_group} STREQUAL ${_install_NAME} ) set(_icon_GROUP ${_ECM_ICON_GROUP_${_group}}) if(NOT _icon_GROUP) message(WARNING "Icon ${_install_NAME} uses invalid category ${_group}, setting to 'actions'") set(_icon_GROUP "actions") endif() # message(STATUS "icon: ${_current_ICON} size: ${_size} group: ${_group} name: ${_name} l10n: ${_l10n_SUBDIR}") install(FILES ${_orig_NAME} DESTINATION ${_install_PATH}/${_icon_GROUP}/${_l10n_SUBDIR}/ RENAME ${_install_NAME} ) endif (NOT ${_group} STREQUAL ${_install_NAME} ) endmacro() # Updates the mtime of the icon theme directory, so caches that # watch for changes to the directory will know to update. # If present, this also runs gtk-update-icon-cache (which despite the name is also used by Qt). function(_ecm_update_iconcache installdir theme) find_program(GTK_UPDATE_ICON_CACHE_EXECUTABLE NAMES gtk-update-icon-cache) # We don't always have touch command (e.g. on Windows), so instead # create and delete a temporary file in the theme dir. install(CODE " set(DESTDIR_VALUE \"\$ENV{DESTDIR}\") if (NOT DESTDIR_VALUE) execute_process(COMMAND \"${CMAKE_COMMAND}\" -E touch \"${CMAKE_INSTALL_PREFIX}/${installdir}/${theme}\") set(HAVE_GTK_UPDATE_ICON_CACHE_EXEC ${GTK_UPDATE_ICON_CACHE_EXECUTABLE}) if (HAVE_GTK_UPDATE_ICON_CACHE_EXEC) execute_process(COMMAND ${GTK_UPDATE_ICON_CACHE_EXECUTABLE} -q -t -i . WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}/${installdir}/${theme}\") endif () endif (NOT DESTDIR_VALUE) ") endfunction() function(ecm_install_icons) set(options) set(oneValueArgs DESTINATION LANG THEME) set(multiValueArgs ICONS) cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) if(NOT ARG_ICONS AND NOT ARG_DESTINATION) message(AUTHOR_WARNING "ecm_install_icons() with no ICONS argument is deprecated") _ecm_install_icons_v1(${ARGN}) return() endif() if(ARG_UNPARSED_ARGUMENTS) message(FATAL_ERROR "Unexpected arguments to ecm_install_icons: ${ARG_UNPARSED_ARGUMENTS}") endif() if(NOT ARG_DESTINATION) message(FATAL_ERROR "No DESTINATION argument given to ecm_install_icons") endif() if(NOT ARG_THEME) set(ARG_THEME "hicolor") endif() if(ARG_LANG) set(l10n_subdir "l10n/${ARG_LANG}/") endif() foreach(icon ${ARG_ICONS}) get_filename_component(filename "${icon}" NAME) string(REGEX MATCH "([0-9sc]+)\\-([a-z]+)\\-([^/]+)\\.([a-z]+)$" complete_match "${filename}") set(size "${CMAKE_MATCH_1}") set(group "${CMAKE_MATCH_2}") set(name "${CMAKE_MATCH_3}") set(ext "${CMAKE_MATCH_4}") if(NOT size OR NOT group OR NOT name OR NOT ext) message(WARNING "${icon} is not named correctly for ecm_install_icons - ignoring") elseif(NOT size STREQUAL "sc" AND NOT size GREATER 0) message(WARNING "${icon} size (${size}) is invalid - ignoring") else() if (NOT complete_match STREQUAL filename) # We can't stop accepting filenames with leading characters, # because that would break existing projects, so just warn # about them instead. message(AUTHOR_WARNING "\"${icon}\" has characters before the size; it should be renamed to \"${size}-${group}-${name}.${ext}\"") endif() if(NOT _ECM_ICON_GROUP_${group}) message(WARNING "${icon} group (${group}) is not recognized") endif() if(size STREQUAL "sc") if(NOT ext STREQUAL "svg" AND NOT ext STREQUAL "svgz") message(WARNING "Scalable icon ${icon} is not SVG or SVGZ") endif() set(size_dir "scalable") else() if(NOT ext STREQUAL "png" AND NOT ext STREQUAL "mng" AND NOT ext STREQUAL "svg" AND NOT ext STREQUAL "svgz") message(WARNING "Fixed-size icon ${icon} is not PNG/MNG/SVG/SVGZ") endif() set(size_dir "${size}x${size}") endif() install( FILES "${icon}" DESTINATION "${ARG_DESTINATION}/${ARG_THEME}/${size_dir}/${group}/${l10n_subdir}" RENAME "${name}.${ext}" ) endif() endforeach() _ecm_update_iconcache("${ARG_DESTINATION}" "${ARG_THEME}") endfunction() spectral/cmake/Findcmark.cmake0000644000175000000620000000241713566674120016346 0ustar dilingerstaff# # CMake module to search for the cmark library # include(FindPkgConfig) pkg_check_modules(PC_CMARK QUIET cmark) if(NOT CMARK_INCLUDE_DIR) find_path(CMARK_INCLUDE_DIR NAMES cmark.h PATHS ${PC_CMARK_INCLUDEDIR} ${PC_CMARK_INCLUDE_DIRS} /usr/include /usr/local/include) endif() if(NOT CMARK_LIBRARY) find_library(CMARK_LIBRARY NAMES cmark HINTS ${PC_CMARK_LIBDIR} ${PC_CMARK_LIBRARY_DIRS} /usr/lib /usr/local/lib) endif() if(NOT TARGET cmark::cmark) add_library(cmark::cmark UNKNOWN IMPORTED) set_target_properties(cmark::cmark PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMARK_INCLUDE_DIR}) set_property(TARGET cmark::cmark APPEND PROPERTY IMPORTED_LOCATION ${CMARK_LIBRARY}) endif() include(FindPackageHandleStandardArgs) find_package_handle_standard_args(cmark DEFAULT_MSG CMARK_INCLUDE_DIR CMARK_LIBRARY) mark_as_advanced(CMARK_LIBRARY CMARK_INCLUDE_DIR) set(CMARK_LIBRARIES ${CMARK_LIBRARY}) set(CMARK_INCLUDE_DIRS ${CMARK_INCLUDE_DIR}) spectral/README.md0000644000175000000620000000321113566674120013636 0ustar dilingerstaff# Spectral Get it on Flathub > "Nobody can be told what the matrix is, you have to see it for yourself. " Spectral is a glossy cross-platform client for Matrix, the decentralized communication protocol for instant messaging. ![Screenshot](https://gitlab.com/b0/spectral/raw/master/screenshots/1.png) ![Screenshot](https://gitlab.com/b0/spectral/raw/master/screenshots/2.png) ![Screenshot](https://gitlab.com/b0/spectral/raw/master/screenshots/3.png) ![Screenshot](https://gitlab.com/b0/spectral/raw/master/screenshots/4.png) ## Documentation Documentation for Spectral is located [here](https://spectral.encom.eu.org/docs/). Build instructions for Spectral are located on [this page](https://spectral.encom.eu.org/docs/tutorial/compile.html). ## Contact You can reach the maintainer at #spectral:matrix.org, if you are already on Matrix. Also, you can file an issue at this project if anything goes wrong. ## Acknowledgement This program utilizes [libQuotient](https://github.com/quotient-im/libQuotient/) library and some C++ models from [Quaternion](https://github.com/quotient-im/Quaternion/). This program includes a copy of [WinToast](https://github.com/mohabouje/WinToast/) to display notifications. ## Donation Donations are needed! Bitcoin: `1AmNvttxJ6zne8f2GEH8zMAMQuT4cMdnDN` Bitcoin Cash: `bitcoincash:qz8d8ksn6d4uhh2r45cu6ewxy4hvlkq085845hqhds` ## License ![GPLv3](https://www.gnu.org/graphics/gplv3-127x51.png) This program is licensed under GNU General Public License, Version 3. spectral/.gitignore0000644000175000000620000000002013566674120014342 0ustar dilingerstaffbuild .DS_Storespectral/flatpak/0002755000175000000620000000000013566674120014006 5ustar dilingerstaffspectral/flatpak/org.eu.encom.spectral.yaml0000644000175000000620000000305613566674120021007 0ustar dilingerstaffid: org.eu.encom.spectral rename-icon: spectral runtime: org.kde.Platform runtime-version: "5.12" sdk: org.kde.Sdk command: spectral finish-args: - --share=ipc - --share=network - --socket=x11 - --socket=wayland - --device=dri - --filesystem=xdg-download - --talk-name=org.freedesktop.Notifications modules: - name: olm buildsystem: cmake-ninja sources: - type: git url: https://gitlab.matrix.org/matrix-org/olm.git tag: 3.1.3 commit: ebd3ba6cc17862aefc9cb3299d60aeae953cc143 disable-shallow-clone: true config-opts: - -DCMAKE_BUILD_TYPE=Release - name: cmark buildsystem: cmake-ninja sources: - sha256: acc98685d3c1b515ff787ac7c994188dadaf28a2d700c10c1221da4199bae1fc type: archive url: https://github.com/commonmark/cmark/archive/0.28.3.tar.gz builddir: true config-opts: - -DCMAKE_BUILD_TYPE=Release - -DCMARK_TESTS=OFF - name: libsecret sources: - sha256: 33ee5dfd3556931b81d47111890c8b9c51093b4ced18e0e87f51c1769e24d43c type: archive url: https://gitlab.gnome.org/GNOME/libsecret/-/archive/0.18.8/libsecret-0.18.8.tar.gz config-opts: - --disable-static - --disable-gtk-doc - --disable-manpages - name: qtkeychain buildsystem: cmake sources: - sha256: 9c2762d9d0759a65cdb80106d547db83c6e9fdea66f1973c6e9014f867c6f28e type: archive url: https://github.com/frankosterfeld/qtkeychain/archive/v0.9.1.tar.gz config-opts: - -DCMAKE_INSTALL_LIBDIR=/app/lib - -DLIB_INSTALL_DIR=/app/lib - -DBUILD_TRANSLATIONS=NO - name: spectral buildsystem: cmake-ninja sources: - type: dir path: ../ spectral/assets/0002755000175000000620000000000013566674120013666 5ustar dilingerstaffspectral/assets/font/0002755000175000000620000000000013566674120014634 5ustar dilingerstaffspectral/assets/font/material.ttf0000644000175000000620000037226413566674120017165 0ustar dilingerstaffpGDEFS$GPOS,6GSUBҩQdiPOS/2 s"cx`cmap1 xcvt D|gaspglyfD}rHheadڋf6hhea4$hmtxjiFloca;)maxp'X name5dzpost2 O"_< ޣޣ@.LfGLf0 +++@+@@U@U5+@@Ukk@+++++++++++@+@@UU++++@+k@@+@"UUUUUU+++U++,@++@+@Uw@kU+@++@U++k@@+k+++3@+[kk++@U+>+@Uk@++@+k++@@+@kU=@+++!k++@@@@+@k++@@+++++++++k@U@+U@+++@kU++3+ +f+@@@@@@@@@@@@@@@+U@@ 5+Uk@@kUk@+U+@+@w+@kU@UUUUWUK+U5+k++kk++++++@@+@++k@Ukk@@@UkkU@+U@+++@k+@55+k@+++kk+@@+@@@+@@@@@@@@kU+@@+U+U+@@+,1@+@@U+@*@+@@@+@@@@@@+++k+@++U+@U+++@WU@+++@+Y@%++@@@+L++@@+U@%UUU+@++++@+@+@@U@U@@U+@@@+++:+@@@`kk+:@@U>kk@k@@U@+Uk+UU+U+Ikkk@UVUkUUwk@+++@@@@++@@@@@UU++U=@k+@+* +@@+U+UU@U+++U++@+@++UUU+U@+UUU@+@U+++++@@+++++@+@@+++#+++@@@@@@@UUkkU++U++@kU@I @@++++++UU@@k+++++U++U@@+@@UUU++U@@+++@kU@ @+++@+k+@-@kk@UZk++4+@U+@k+U@@++@+kk@+U+U+UUUUU++++++k++@@@+5'k@+Z+>kek+@++@@U+@U@+@+Uk+?+@++UU+U+++++, , x@89_z!$,19Sqmd8.<6r E:Qs+L0_a#(.3;UE& 0-9# 4MSu; (     iGA;73,(QPOtr <09__ az(,!-#$4(,6.1;39?;SFUq_|Em&d JSWX Z8i.0<-6#9r-g #E 4:MQSsuJa+;L  !"#$%&'D)))19AIQYaiqy !)19AIQn U 3$`6q#/Fh&eM[  ( P f v E s ! F 0{5c6Y)HvM`{4q4YMu/`"R|Y :t FZ.Wq+Jl7k+Il  3 Q m !!4!n!!!"2"Y"""##6#V#v##$ $8$q$$$%%L%%%&4&e&&'*'P']'s''((b() )N)x))))**+*:***++P++,,7,--<--.^../*//00B0h00011;1Y11112#2_222223!3A3X33344>4r4444555b5y55566&6v6666737E7w77788Z88889 9.9a99: :0:Y:::;;<;e;;;;;<<*<<<<= =&=[===>>C>s>>?(?K???@1@U@~@@@AAEAcAAB'BPB}BBCBCDEEEEFFCFgFFFG/GeGGH%HcHHI*IRIIIJ J-JRJtJJJKK:K\KKKKLLLILgLLM$MmMMN2NyNOOaOP P0PnPPPQ#QQR'RIRgRxRS$SST1TwTTUUyUUV VV[VVVWWDWuWWXX5XxXXXY YY-YzYYYZZRZvZZ[[c[[[\7\p\\] ]8]g]]]^{^_,_w__` `f```aaSaabblbbcc]ccddIdndde=eeff=f|fffggHgjggh h0hhhiRiijj,jrjjk k4kSk{kkll+lZlllmBmwmmmnn>nlnnnoohooop p?ppppqqgqqr4rlrrrrrrrs ss7sHsYs}ssst t1tMtht}tttttu,ueuuuv$vYvvvwwZwwxxZxxy$yNytyyyz7zrzz{{W{{{||4|T|||})}g}}~~,~L~~~3j&Pzh*f̂=z,d܅:X܆GMч-Nu)` >wɊi-rUэU܏1Lvߐ%6VݑXzM "V!6p=̕0mAnƘ*mH|,Ipƚ3؜'Q+Z.jʟ*T#Yՠ7m0Tr٢,Q$R¤!lϥWW>8T)  @U/?54&##"332655##53#54&##"332655##5372#!"&5463 @ @ ++u @ @ ++ V  @  V  @ UU 3#!5333UVVV*V@뫫V@@ 5#35#535#572#!"&5463@UUU@+++*+*U#57#'7++ 3#'7##5%'53`uu@`@@ @@u@@@@`@@u@@ @@u@+5462"'&472653#"'&'&'&'&5462#4&" ,,r88EE+2#))W~V+=Z>$( *,,88EEo#27%)*?VV?->>- "& @U#3%5354&##"33353265##5#35372#!"&54635+ @   + +@@ V V 55++U%55"&5472'654&#'7UUFeK5FeK5UU@UV@eF2)!5K+eF2)!5K@UVk@3#5&&5326"&55462q$K5*5K$C\CW4&&4&6QFFQ6/==&&&k@(3#5&&5326'326574&""&55462q$K5*5K$C\C 44&&4&6QFFQ6/==  &&&@@!''#5&&53327'#"&55''5462'65[eY!*5K$C.#&&4&U YFFQ6/= #&&&%!+U3#!"&546333'33'33U*@**+@+++@VVVVV++ $5#5##33572#!"&5463!!"&5U+UU+kV++UU+UUU*+++ $5#5#75#72#!"&5463!!"&5ՀV+k**++U++U*+++&!!"&55#&#"265572#!"&5463U+UU ,k*+*u ,vj!%5#5#%'''7'7'777***4M(II(M44M(II(M뀀V++k;ODCO;>1qe11>>++ 5!5#5!572#!463+UU++++k++U@k !!5!%5!+U+*k+++V**++ 5!5!5!%'!"&5463!2UUVU++@++@++U+#'%54&"6"26472#!"&54635!!5kIDI(({V  ((WU++++@@ &26%2#"&5463327675#'3##'#535#53#7#5 K  //^/ $(@+@@++@+V K Ֆ ($ /\1/  +@+@@@+kk U'/7?GO2"&42"&462"&42"&4&2"&46"&4622"&462"&462"&42"&4""""""""f""""""""""""""f""""f""""""""""""""+U5'72#!"&5463U+kk+j++ ##463!22'#"&55!5k U  U U  V+ @ U +k@ %3'353'##5#U@UU@*U@*@UUU@ '7'5''#"'&&7'71Oy&fL:/W$M lDG. EAR DgD.W>0ENk+264&"&2.54,, |W,+4'  ,,W>PF=EAR>++ 5!5!5!2#!463+UU++@++@++U++ 2#!463UU++%!72#!463++U+*U3 '##"&55'%'732Nu( 8b2)82@@7632#"&5463320]/ $( K  ]0/  K Ֆ ($ +5"2B'#"&57'&&547'&&547'"'632'654&'654&#"'632Fe" *#!.'19,,+%2=X} e1#2# #5K"1%;#&.6.O%c:J9-}X=2')Fe# #2# K5@/%&4737'7'54&""26472#!"&5463}# *,,* uXPX4&&4&V,*+!4,4!+*U%%&4&&4f*@ #)5#54&""26472#!"&5463'57ժXPX4&&4&V@@@@%%&4&&4f*k**++C *&'77#5'7"'&'&55&""''&476 JLn*LLU5 .h. 5i"/JLjj[KL5BB5d[+#%5#72##"&55463&2&"'62&"@}  z  #E88:,~,ZU  E88=,,k!!%463!2#!"&5*kk%#2##"&5463kk*k!!%463!2#!"&5*kk%#2##"&5463kk*+k!#"&554&"3'354622655#U@2F2"@UU@2F2"@V#22#UU#33#++ 5##5##5#%2#!463k++*++U++++++U#$264&"264&"$2#!"&4623&54l>++>,>,,>+/bDD11DDbE`,>++>,,>++>DbEEbDD1+ +16264&"73##5##"&4632""+U] B*5KK5*B""T2##"&5533##5463264&"7#'##"&7'&'''4775'&7763677633276++&""|+ +@+V+@""%  $%  $@ #7##!2#!"&5463*UU*VUUUA,V*5 &%5&#"6322#"'&#"&#""#"&56326!*A44A''L*)=A4+J60+KJ++u   9 +U7!5'2#!"&5463VVkjj+U%7'633!53"&55463!2VVi+UUUVPP.n:z++!%767'''!53"&5547''7'"'!23#1" n:}U!v"Vp;( "[:+ ! v O-o+@k 77#53#5@wb+Mw+bUU2#44#4&#462#"&UX|=Y>YÉq(kk %##5#53533**뀀*@@ %5#5##33572#!"&5463kV*VV**VV*VV*++ %5#5##335&2"&4kV*VV*m}}}*VV*VV}}}++6264&"2"&473##5#535eeeS}}}VV*VVUeee}}}V*VV*V@@ !'!7#5##%#!"&547763!2m&uJVJ+    u++   $ @ %'7''72#!"''763LLLMMMML^ssMMMMMMMM+++%2654''7&#"62"&4Fe$-o$-76'&( % #  09*)54>!(<7 ;  #2  t%&&  $ ('$$<7"5E>'!H  I 0$$"@@ %5!32652#!"&5463V&4&U&&*+k#2##53264&##553$33#"&4633#"k,>>,VV''Vj'VV,>>,VVk?X?)'6')**06')?X?)+U5'72#!"&5463U+kk+j+U5'72#!"&5463U+kk+j!k7#7&#"'6632LN0>8Y2uIUMM(A4DVk%!5!**++ %5#62"&4k}}}**}}}++6264&"2"&43#eeeS}}}jUeee}}}C*@U&#'7hqM@[mWU&#'7'7hqMUU@[mW@UU@@@ %5#264&"#'57* lpppp뀀\ pppp@@ 5#264&"#!"&5463@{4&&4&U@UU&4&&4/U*@@  $(,16:>BFK35535353753'5353'5353#553"&532#'#55353'53'463++++*++++++*U+U++*+*+*+++@++++U+++++**++++++UU+++++++**++@75%%5+@@++@ 75!%!!53@**+++kk 3#73#'!!(P],e e,*k&//Z*+k2&&#"#56 It2Y8>0NL?UVD4A(MM7++%3#3732#!"&54637T-m(m-xo,,u@ Vvv@@ '3535!32652#!"&5463UUU*VjV&4&U+VV@@k&&*@@ !'!33537#!"&547763!2m&uJVJA    vu++   # ++ #77'355232#!"&5546335463UU@@VVVuVU@@ **+**k'2!54&'54632#!"&55462!54/""*. ,, .jkk@@+U!537353##"&%3#53#3#+@UUU**+*++U64633#"3355#"73#53#53#+Q9KK(88( @@ 9rQ*8P8*@@++++*++6264&"62"&47'5''7'7|WW|WEpppUeCbbHbUX|WW|qqqp2bQ QQ T ++ '5264&"2"&4 `p1eeeS}}}kp9Deee}}}++ #3##5#535264&"62"&4%'7'7@@*@@)|WW|WEpppbbb@@+@@+@X|WW|qqqL!S R R++7''575577''5462@P{+KJ*kDzO  u5*Pk*N  ++''575575462+KJ*@5u  u5*ku  uk+k%5#5#2##"&5463353***c   #VkkU+++  G **+k77#5372##"&5463353U+U+c   #VUv  G **+k2##"&5463353N   #V  G **+k2##"&5463353N   #V  G **+k,%654&"34623475#2##"&54633531&4&  "(b   #V&&  a))*  G **k+z%'73#5'7'753=((=\\zbwwb)QQ)\\ybwwb@+ ''73#5'7'753'7++*.((=\\zbwwbk*++++++\)QQ)\\ybwwb+++U+ %7'''#5'7''53'7((81\bw*zA"|()1[bw E+kyA#@+!%'73#5'7'753'64'7((<\\zbwwb!V1 )QQ)\\ybwwbd2z4*X*V1%3'#3737#'#5'7537371)E*E)DFFdGGdFFdGGd*rGGdFFdGGdFFNN2"&4264&"%#'#5'75373F22F2 jKKjK+FFdGGdFFdGGdU2F22FKjKKj|GGdFFdGGdFF6264&"#'#5'75373jKKjK+dGGdFFdGGdFKjKKjdFFdGGdFFdG%264&##'#5'753735KK5dGGdFFdGGdFKjKdFFdGGdFFdG++%27#"&5467'654&'H,8@lX}oQ6JWSQo8 J6k8!W}XSy@T8>WiyS0'!8TU '%53##"&5537'7'7'7%#54632#5k**@bbCCCb**k*UU*QbbDbDDb*UU*U %5#72##"&55463%3!535463!Uk + +@@+@ '#57#5#57#5!2###5#"&5463****kk++U**U++U**+++'6264&"%3##5&&'#53667532"&4|WW|WT,,`C*C`,,`C*C`F22F2kW|WW|S*C`,,`C*C`,,`2F22F6264&"%3##5&&'#5366753|WW|WT,,`C*C`,,`C*C`kW|WW|S*C`,,`C*C`,,`/%'327'#5&&'#53673#'654&#"'6753[W>2e,-7*C`,,%S,, W> %*C`)2>W:,%,,`C*7-d*' >W ,,`/%'327'#5&&'#53673#'654&#"'6753[W>2e,-7*C`,,%S,, W> %*C`)2>W:,%,,`C*7-d*' >W ,,`6264&"%3##5&&'#5366753|WW|WT,,`C*C`,,`C*C`kW|WW|S*C`,,`C*C`,,`@+ 3#3%533'3++U++*++VUUVVVU++%!V+6 '07'62DyyTUU++,!3#35#"&5475463!2#!"&5463U*@"V+0  0VV++"'073#"&5553##52#5#5"&4627!#54633U**V@+@+****}O9U+*@@ 3#53'#5353cyy]NxyxxN]ݫk!1354&""&5546354622#5!2#!"&554634 $ jU+  @  @ k1%#2##"&5463354&""&5546354622#kQ4 $ k*  @  @ '?354&""&5546354622#7"&'3%"'&47762'7'7fI , Qd Ep   4-yx/  V  V Qc;a    4,xy/ '77"&'377"'&47762'72#&&Qd Dш  Qd D6Qc<`    Qc<`U+ 5##5##5#72#!"&57+++UVVVVVV@&%!2#!"&5463"&5467363232#&!.*j,V*&$+%,++7++V++ 7#353'53+U+***+**U3 '##"&55'%'732Nu( 8b2)82++!!VVC '!7'%'fo*u+f6%62yy65UU +#9%7>2&#"54&"32##"&554635462KK  $'-.-'$  ,-=V k .8^6      8=M  U U 6 '67''632F HGS!-+t-, JGg5,[ +U 355!%5#'!!355!U+U+*V*+U**@VV**@V**@VVf&,3###"&5475#"&55&546235#7#35#@U@(@&@+@@+@kV*AA, ,VV*+)%54&"32##"&554635462'"6 &V k , 6,>Kp p9  U U j>,=cUUUL+2-52'6654&"&&54'654&"&&54622"&4}91'/dd.'19U#*2F2*#KjK""|Y:c%O.FddF/N%c:YY#;%1#22#1%;#5KK ""'3"&5462"&55326554&"265` DbE3F2,  , 3F21EE1 #33#  #33#@`(#5&&'332654'&546753#&#" )"@!+/;@d* @!#/4  '..*!-$A)./,-@@ 5#5#'5#5#!!*+U@@ #'+/37;?C7#55!%#5%53'3##553'53'#5##5#5'#5'#5#5'#5#5'#5k++U+++++U+++U++*++*+*++*++++++**++++*++++V**++++**V++U++**++++U**@@ #'+/37;?CGKOS5353535373#5335353'5353'53'5353535353353'5353'53'53@+++++*++*+++++++*+++++++++***+++++++**++U++++U++++++**V++V**++U++U**V++U++++U++U++**++ 5!!'762#57*P*2EPUU*P*2EP@@ #'+/37;?C%53#53'53753'3#5!53753#5#57#5##5##553'535#553++*++++*+***++*+U++++@++++U+++++**++U+++++U++U++++++++U++++++ @@#'+/37;%53533##5#535533#53'#5##53#5535#553#53+++*U+*+++U+++U++U++++U++**+++U++++++++++++++++@@ #'+/37;?C535353'53'3#5353533753'5353753'53'535353@+++*++++++++*++*++++++*********++**++**+++U++++**++++**U++U++++U++@@ #5#5!!!%#5##57#5+*++++*****U++U*U****V++@@ #'+/37;?C53'5353753533#'5353'5353'5353#53753'53#5353*****++++*++U+*+++++*+*++++*+@++U++**++++**V++++++V**++++**++++++@@ !53%!!#%535353#53353#53++U+++*+++@+++**V++U++++++++@@ #'+/37;?C%53535353753!!53'5353'535353'5353'53#5353@+*+*++*+U+++*+++++++***+++**++++++U++++**U++++++U**++U++U++****++@@ #'+/37;?C%53535353'3#5353#3753535353'53353535353@+++++*+++++++*+++++++*++++++**++U++U+++**++U++++++U++****++U++U++@@ !!3#5!5!%3#@UU+*+U**+++@@ !!5!5!5!5!@+U++U**V++U++@@ !!5!%5!'!5!5@+++**++++@@ !!5!5!5!5!@+U++U**V++U++{%264&##53264&###32 K@  8.-"$1@@[4"/+2H+@!#'7#'''#7'+|"-3<:4y!@4@P,$<yN{!5!!"&5467%3'#"''&477'7+" g u v n3UU / & -fP  u u  n3U= '#"&547''7p89%/5KG+:+G8 K5"6G K6{ 3''3#'##!!f3*u00u9@@@U@@ 753'53%!!5!%753UV**U+++++U++@@ 753'53%!!53%5!UU**U+++++UUk++ 3#3#537#ի,5k+UK5++5U #3!57'5jj@kk@++@@ %5##5##5#2#!"&5463k++*++VV֖+*++ 5!5!5!2'!"&5463+UU++@++@++UU+3''3#!"&546vv@u V++ %$"&'3&"&462"&462264&"2"&4%J; eeeS}}})!!aeee}}}@@%5!332#!"&54633533#5++kk+*++kk+k#2##53264&##553$33#"&4633#"k,>>,VV''Vj'VV,>>,VVk?X?)'6')**06')?X?)@@7!'#!"&5463!2J*`J*``5*wM 7#'75'7``KtIIU``swHH++ '!"&5463!2UVU@@ '7627#'P'2PPj'P'2PkU 77##5'!!kUU*Ֆ+U@3!53!++@U@873254&'"&&##5!##"'&'&574#"#4&5&547632>1 SC$'3$ b "156$++=2x-  %)U@ 7!!%'353UVUU@*k+VVU !!%'3537##5UVUU@*jUU@**UUVVUUVVU@ !!7##5UVVUU@*+VVU@2##'73264&#!5%!553k#22#+@@0V2F2+@@+"*****W@"'#5&&'3327'&5'"'6753#&r7/!@!,/<%KSI@ #/0..*!-J;I!./,-U@75!5!5UV++++ "&:73'##73#735#5#'535#5##35#35#3#5##535#533538%K#IH"`++++******++++ր++րRm*++++****+++++Uր++ր++K+ 7%773#5!#5j---s**k@t-..-@kk@jj+52#"'#"'##"&463236236$ > H > $$ > H > 5, , U@73#5!!UV++5k##5#5'!###@@@k@k@@@k@++"*%654'&546323&'5##"'#32"&4CZ8-*;9%Y95*C9v}}}~ 6=(9**#8 ! (9*}}}kk!###k*u@u@+k%646332##"&4633#"33264&##"33#"+D1#22#  ,,1bE3F2, +",>++U#!"&54676632):?,5KB0L-:X*=*,?K51I'0I++%264&##4&#"'"32"&4` 2#-&&}}},#3%&4&*}}}U77''%#!"&54676632Սo,):?,5KB0L-:Xn,J=*,?K51I'0IU%#5##7#!"&54676632k@V@k):?,5KB0L-:XUUk=*,?K51I'0I++#"337'#"&5467'654&##54&#"'632%#22#e+5KH3"):-!& D1 '/:X+3F2*K54J+=*7 && 1D IU&%264&##54&#"#"3%#!"&54676632&& D1(> #22#):?,5KB0L-:X&4& 1D0%3F2=*,?K51I'0IU%3'337#!"&54676632+@kk@Vr):?,5KB0L-:XjjV=*,?K51I'0IkU 7!!%'353k**U+땕kU 7!!75#7#k*UUU+V+U32#!"&5463+++U%5!2#!"&54633V+++U!%54&"6"26472#!"&54633;4;f""@+""Z++U %5#5##33572#!"&54633@*@@*V++@@+@@+@'2#4&#2#4&#2#2##53!#5463a+qO>X+?,&@++aPp+W>,?+&+*@@@!).2##53!#54632#4&#%#&&'52#4&#2#++a+qOx_?V>X+?,&@+*@@aPpk?`#W>,?+&U!53!53"&55463!2UVUUV+++%5!2###57#"&5463**@@+%!2##3#535#"&5463***+**++@ /3#'3#73#3#!%#3#3##!"&5463!23UUkkUUkk*****+*kk@@V**+*++***%5#2##"&546353U*+*++++ 3#'7##5%'53`uu@`@@ @@u@@@@`@@u@@ @@u@U2##5354&"3#"&554p&@UW|WU@&qO&+>WW>+&O@2##535#5354&"3#"&554p&UUW|WU@&qO&++>WW>+&O +k #'+;5#5#'5#5#5#'5#5#735'3535'3572#!"&55463**********@******+**@**@**@**V++**@*****@**@**@**@Y7'bbYbbI%'7IbbU77'7bbbbU7'7'bb@!'7LL*MM 75!''7b++b + "&*.>%'375#5#'5#5#5#'5#5#735'3535'3572#!"&55463U@**********@******V++@++@++@++U++++@+++++@++@++@++@+ 3!'7!+MMkMM 3#'7'7!5!**M/M*k+3#5&&5326"&55462q$K5*5K$C\CW4&&4&6QFFQ6/>>&&&U!53!53"&55463!2UVUUV++U%5!5#!3!53!V++@++@U #6264&"!53#!"&53"&55463!2  VUVUVk  U!53!535"&55463!2#UVUUV++@@37;%5#%#3####5##5#"&55#535#5354633533533235#7#5k++++++*++++++++*+++*Uր*++++++++*+++++++U**UU #465!"&#WWVee?WA`UUFeeo`Ak%#5#2##"&5463pV&&&&+@&&&V&k %#264&"2##"&5463USv+U  j U %5#72##"&55463%3!535463!Uk + +@@+#"&2##'35#'5463%3'!53547'!'! @.U+ Q+"2+ '+DU @o+Y %HQ"2@ 'B+@@ )3%5##5##5#%2#!"&554633537&#"'632&#"'632@+ * ++1! ()1)10)-=>-++++++kUUUU $$--@U%5##5#%#!"&55463!%7+*;  ++++ uUm(@ &&55667#5nRRn;RVYYiCBzU+ %%5#'5#5#'5#5##5##!"&5732k++***++++UU+**UUUUUU****@k%#2##"&5463kk*k+)2"&4264&""3264&72##"&54634&&4&X??X?kZ&4&&4?X??X""*VU!13#"&5462"264&"6"326472##"&5463 ,, F22F3g"/+U, , 2F33F$$D3U%!2#!"&5463U+@%!5#2#!"&5463V&&&&kU@&&&&+ %!264&"2#!"&5463kUVj "&53"&4632#5462E`F0EE00EE0E`F0EE0E`FE`F0EE0@%!2###5#"&5463kk+++U264&"#'&4773KjKKjAAA5jKKjK.4zz22zz@@%3#5'#5375&&5462kUkUUkUV&4&kAZZAkUD && D+@ %53!53!53'!!Ukkjjk+++++++U)7%5#72##"&55463264&"7#5&5475'3#"&5463!Uj wJUUU  b&&&&+++264&"264&"'5#5##335%2#!"&55463B@@+@@+R*@@*@@++ $5#5##33572#!"&5463!!"&5U+UU+kV++UU+UUU*+++"&462264&"2"&4@&4&&4`eeeS}}}4&&4&eee}}}@%7''2##'#"&5463(XX((XX(U@@U((XX((X@@@+k@ 3#'##33xx**k@3###"&4632U6%(88(@$18P8++'/7?GOW_go$2"&42#"54264&"2"&42#"542#"54'"54322"&4'"54322#"542#"54'2#"542"&462"&4"   feeeS}}}@ 5   @ K J 6 A     > Keee}}}# U  , @ J  b  @@%)19AIMU]e$"&462&"&462&"&462"432'"5432%!!"5432"&462&"&462&"&4625!"&462"&462"&462      @ +   ]K    ]  I  I  WV u+  >C++  a  l55 !'/TZbjrz62"52"&42#"4&2"52"&4$2"52"&47'"&4632'#"&5467'"&4632#"4"&462"&462"&462&"&462"542'"4323"432&&'5462#6"&4625B  j B  J  [P  <  <   i         K     @  > @   @  Q  <  <    b  b    W   `  55%-3;CKS[ciou{2"&42"&42"&462"&42#"462"&462"5&2"&42"&42"&462"&4&2"&46"&462'"4322#"4&2"57"432"&462"542$2"&42"562"&42"&462"&4?  v   >        )        6B      KCM  J  W  I    b    > J  W j  T     b  ++6462"+}}}}}}k+ 2#"'664&'6Y}}Y:019910}}ctc+ 2#"'664&'6X}}X"BSSB}} qq  %264&#"7#'#5'753735KK5!))!FFdGGdFFdGGdKjK ?L? GGdFFdGGdFF6264&"#'#5'75373jKKjK+dGGdFFdGGdFKjKKjdFFdGGdFFdG%264&##'#5'753735KK5dGGdFFdGGdFKjKdFFdGGdFFdG2"&4264&"%#'#5'75373F22F2 jKKjK+FFdGGdFFdGGdU2F22FKjKKj|GGdFFdGGdFF@@ #!"&5577'''5463!2@@UV@UVU@* @a@VVތ@VVVAa+@ '7622#"'2654;4&2#4!;&#2+++ %7667#"'3&&7#&54''7#7'632NN;LO.C l8fO.CN;L0#0GS(S= GO0+U#6264&"332#!"&54633462"X??X?+'DD#(8((8?X??X*8((8(k'+462552##5#535#"&5463"&4623#IDI@@jj@|""jj+@@+*+*""*k $264&"72##5#535#"&54633#"}@@jj@jj""@@+*+*U*++ 35#5#'5#5#'5#5#3###"&5463354633232++++***++++V U @++++++++++++@ @@#+%53##52#5#5#546333#"&5562"&4+UU+U+UUUUF22F2kUU+UUU++UU+U+U2F22F@@!*36264&"62"&453##52#5#5#546333#"&55""F22F2+UU+U+UUUU""f2F22FUU+UUU++UU+U+U++ 3!!"&57!'#!"&5463!2+*+@V?*UjO@@4264&"&264&"264&"264&"72##"#"&46h-W-Oq?,%PppChdG,>  pp@@77'%'#57'7762)OC)e)Ck)C)e)C@ 2##5#5553#5#"&5463jjjVjj++j+@+*+*++6264&"2"&473##5#535eeeS}}}VV*VVUeee}}}V*VV*V@'$264&"62"&4&&4673##5#535|WW|WEpppU.'8HH8'@@*@@kW|WW|ppp$XI.dzd. @*@@*@@%5!%2#!"&55463*@U%!2#!"&5463*+7!##5#"&55#53535#532VV*VV*֫*VV*Vի*@k%5!2#!"&55463*@k%5!%2#!"&55463*@@%!2#!"&5463*k*U*@@#2#5#553##5'3#"&=4633##+UU+UUUUU+UU+UU+UU+U+U@k%5!2#!"&55463*@@%#7!2#!"&5463*K:**eL3C*U*k@%#2##"&5463kk*U*UU%!2#!"&5463++ !!!!!!+VVV+@+@*@U7%!xx+@@ '7627#'P'2PPj'P'2P++"%3572#!"&5463#53533##+++**+UV@**@V++++*U %#55733#*@d$$*+ 3#%23#5767654'&#"#476+4V=Y  ..*K B$ a  1$U%#55733##5#535+@dVV*VV$$*V*VV*V+ 03##5#5353#5767654'&#"#4767632UU+UUY   .  kV*VV*V$ a   $# Y)4'&'&"327655432#"'&'&5+      XA((((98(9t>,;?"%!2#!"&54635#53%!!"&5++UU+V+ի*+U(1%#546335#532##!2#!"&5463!!"&5k+UU++UU+U+*+*V+V+V+U!%5%##535#535#532#2%!!"&5!2#!"&5463kUU++UU U++*++*  +U+V+!%!2#!"&5463!!"&5#7+UU?L;*+V+V+UqdK2&%!2#!"&54635#53353%!!"&5+U*++U+V+UUU+U.%##535#53#32%!!"&5!2#!"&5463kUUUV+U++**++U+V++435"&554633#32#!2#!"&5463!!"&5++VV++UU**U*+*++V+V+U #%#7#53!2#!"&5463!!"&5*UUU+UU**+V+V+U%)9B35'35"&55463"&5546332#2#!2#!"&5463!!"&5++++ + +UU**V++    ++V+V+U+45#72##535#"&55463!2#!"&5463!!"&5@++UU++UU@++U+*++V+V+U#7@5!!5##5#535372#!"&546335###535#"&5546332'!!"&5++*++*+V@@@U@Հ**++++U+@+U@@%#72#!"&54633k**@@"+2"&453##52#5#5#546333#"&554&&4&+UU+U+UUUU@&4&&4UU+UUU++UU+U+UU*%264&##54&#"#4&"3%#!"&54676632&& D1:$)5+2F22#):?,5KB0L-:X&4& 1D/ E,#33F2=*,?K51I'0I++ !!#'#2#!"&546337+`JKaVVUUU++KKV+UU!77'+*`"<Q%!2#!"&5463!!"&5+UU+V+V+U ,, %+1777&7677673&"&462&'7#67&'7'&'5'67y%/?i/$3?r+)e&4&&4)M+)N)M%/?i/%3?[+%)k&->34&&4&U/$3?~%/?3r/%3?+%)1@6264&"467'&&56&5467676'&'&&547F22F2z-%"@@ ## @@""@@ ## @2F22FT&E ;!%%#;;#%%!;;!%%#;;#%  #'753'777&2"&43#7'7'#5'7#5*...4&&4&..U*'.....&4&&4*..c.s**@ 3'7#'##7!335#g2E)D)EUU@]N*++++'53''5#5'k!VOXM@j+9.YOk+k335#VV@ժ @ '+/%53'53'535332#!4633#3#"&5%5353+++*+*+UUUUU++@+**++++*++++++ @@+/3?CG5!3335335355##5##5#2#!"&54633#73#'33#5##5335!#3*++*++++*++*++++U*++*++****++++*++++++@*+++++*****+++UU'/7?2"&42"&42"&42"&46"&4622"&462"&42"&4""o""o"";""""""""o""""<""<"";""""""""<""   $=%3'5'#'5'#5#33'!"&5'35!#'!2'5#'35#'5#'35#'U* J* JVVa++UV+J*+JV*+JV*UJ VJ VVVa6+J+VV*+*VJ+*VJ+ ++ #35#5#5#5#5#5#5#5#5#2#!"&5463VVVVV*VVVVV*VVVVVVUVVVVVVVVVVVVVVVVVVV!0'#5'#5##53353'#'32'735#'532#'#5b6H + +u (  `++K  cI i55++u  ) `i -+@@ +%5#72##553#5##535#3#'##532**  J@ + @+K  K @@` @ ++55+  -++ 6264&"62"&4$2"&4Z""F22F3!jKKjK""f2F22F]KjKKj$264&"62"&4&2"&4HF22F3!jKKjKF22F32F22FKjKKj 2F22F** 'L%7'6"264264&"&264&"'7'"264#"''"''&477'&47762762cNNM  I    $MMN  eU\ TU]UU]UT\NNMNz  I    NMN#  4U]UU]UT\UU\@@7!'#!"&5463!2J*`J*``5*+U#%!2#!"&5463#5##5#57#5V+*+***+++++V**V++@@"%#53733535#5#72#!"&5463kkk* + ++  J* ++ ++`*!77'+*`"<Q@@ $)%463"3463#463"#52653#5265##5+W>,?+&aPp+W>,?aPp&@>W*?,&@a+pP>W*?,a+pP&@@@ -363"'63"'657'#47'#47'#527'#5277'65H9?/*B" "e=*++85BVE5)2"X " "+B" *?9*/="2)5DVB58++*X" ++2"&4}}}}}}@@(54&##3#3#326554ੰ#!"&5463@VV++VVV ++*++ * 2#4&"#462#4&"#4Š+pp+zX*?X?*aOqqOa5X>,??,>@@ %5##5#32#!"&5463@+*+UVVV+*@@!5#3#326554&##572#!"&5463@UUU*@+++++*@@#'5#"3326554&##572#!"&546353@U***@+++*++@@%5#32#!"&5463+V+++*@@$54&##3#"35#532672#!"&5463@UU*U*U+++V++*++6264&"2##"&473##5#535eeeS}X}VV*VVUeee}X}V*VV*V+@/%#2#5"&463264"32#!"&5463373->>-((((->>-E&&k=Z>&(:'(:'&=Z>*+++U3#!"&546333'33'33U*@**+@+++@VVVVV@ 3#"&4632U3F22#U#22F3 k+%3!535&&5462ր6GX|WLS**T S7>XX>9T++%"&462%3!5#546332#35&&5462nYL8@ @ 6GX|W9TS*jV V@T S7>XXI'7IbbbbbU'7'Հbbbb@@4264&"&264&"264&"264&"72##"#"&46h-W-Oq?,%PppChdG,>  ppU7!'#!"&5463!2J*`J``5++6264&"2"&4eeeS}}}Ueee}}}+U!2#"'&"#"5432276#"'632 `` ``N]XSRYXS##:##7U+ 73&47##!"54764'&543!2##:#USSRYXk `` `+U2"/&4?"2764']]]VVVBBBB9r99r9@@7!'#!"&5463!2J*`J*``5*U+ 7!'''7572#!"&5463R@.@56kmR7 *V+U#6264&"332#!"&54633462"X??X?+'DD#(8((8?X??X*8((8(++ 3!!"&57!'#!"&5463!2+*+@V?*UjO++ $0@53'!!"&55375#3535#554&##326'54&##3532672#!"&5463++@ 555k 5   @@u*+K * @@ 7 *@@#%!2#!"&5463#5462&"&462*B,++,>ZZQNNNr888 ,+|,,EZ[ENNN<WW 5664&''77&'&'7#67?WW?.==.aa~09A+``+G^GS_a+%._-&UW  %673677#&'7'5&&4675h+r$0+Wa.==.?WW?.%%G_SG^G+``B@@%!2#!"&5463*jkkk*U*kUU#%5##5##5##5##5#%2#!"&55463+*++*++*+UUUUUUUU0 &75#"&6264&"#"'&5477632&&77}J"  C  j  4[  !A  A }+U %7'#55372#!"&5463373@KKKKkD''KK66KK6**+k %7'#55377'#!"&5463!2KKJJkUU + KK66KK6JVVK  ++ %$"&'3&"&462"&462264&"2"&4%J; eeeS}}})!!aeee}}}@@ 777##73546335%&'*U*U=`@=+U+U*=++6264&"2"&4$"'75eeeS}}} LJj&ZUeee}}}(LhL&Z~U\%54'&#"2766'432#"5%"3#"'&533254.'&'&547632#4'&%73#5  ,{S)RS , 9)&!# * !** Ie+@6$45   Gm9)nn5  %'    &:%Y~8^"23#"'&533254&&'&'&'&547632#4'&"&53324##5327654#"#47632v , 8(!* !** *J/+ *. & *0&*!  %'   &*%&" R! & -$H@+6264&"%"&4632753#5|WW|W+*pppPA7*+UX|WW|5BOqqq+ ++%/%27''#"&547'537#5'654&#"'632'$W{628Pp ;*+* W>(" 0:C5U")>XV6 qO:0;YI++L5B:0")>W *++ !)%67#67#67#53&''3&''3&2"&4p  > p>J?WW}}}@>@>R``v}}}+532#5#3'35#"&55#535#7#!Հ+U+@@*UU+@@**U++@@+++@@@@ #533##53%3#5#53#533#3#3#@+UUU++UU**Հ@+*+U*++*Հ+**** @k #'+/3#53#53#53#53753'3##5353#53#53'53UUUVUUUVVjUVUUUUUUUUUUUUUjVVUUUkVVVVVVkUU@k !!5!!53@kkU!73'#373%3#'#'#"&4632373%7(D+D)D',% &R1GddGR3 " * +4ee@NNU#!"&54676632):?,5KB0L-:X*=*,?K51I'0I!!%773#'"&546753#553'7p&@@k#KjK#@*&&}&+:#5KK5#:g++??S'' !#'77532"&43#7'7'#5#57'7L&&*JjKKjK+@@;&&&*@{&t''5??jKjKKj +x&K&&5??++y&++5#772#!"&5463!!"&5k56V+ U*+@7!''%2#!"&5463k*`J6 `@*@  $)-16:>73'''!!"&53#73#'#463#3#3#%2#3#'3##553#3#@D5'`+V**U+++;****+++++++++++k[E.fU+++++++*+*+++*++@  ',049=A#53#5#57#46#5#5#5#5"&5532##5'#5#57#5#5*++++e***++++++++++++++++**+++U++++++U*++U+++**U++@6264&"%2#!"&5463dddkKjKKj*L! 7'77'7#53''3#5!j&D&B&y**&***V&$&'z?_&[?ꀀ%17!##5#"&55#53535#5322#&&''267"#"&'3+++++d D5Q_Qd D+++++ր++c<`Q6Qc<`++$,4234&#264&"73#!"&54633732'52#4&462"U*X??X?jD'5K;(8((8*>X??X+@gK5);:((:'+ $064632"264&"'535332#!"&=33##5#5(((:X??X>+@'D+@@+@:(((&>X??X@@+ր@@+@@++U(7''7''3#!"&546333'33'33i,,,,e;;;;U*@**+@+++@,,,;;;+@@@@@@@"''77''773#!"&54633#!::::k,,,,*+:::*,,,,*+k73''72#!"&5463#3#3#D6& j**V++ZD.  *@77''%2'&5463L*Ltt++ %7'#"3537"''&47762+JJk *VJK5 U@+%-5=$264&"62"&4'#5'&54776323"''264&"62"&4$"&462v>,,>+Z>>Z=E/*E <) ,?->++>,Z==Z>\""K+>,,>>Z==Z^1j< < ) +-+>,,>>Z==Z""U@ 15!264&"264&"'5462##"&55###"&55&+XX   kk3""3&  &@@ -!'#264&"264&"%##"&55!##"&5576332k* ),   ,`€  %"575'&77546335332#"'"'#3#"''##532727(@@(1%%`%%1V**-)UU)-**.('\'(U**U c@@c ****++,,+U@ )5#264&"'5#264&"2#!57"&554k=Uk-X, ,kkmkk-"3+ +3U@ 5!264&"'5462#!57"&o""XX, ,+jj""3##3+ +U@ )5#264&"'5#264&"2#!57"&554k=Uk-X, ,kkmkk-"3+ +3!3735'735"''&#"#3576"&462<-',+- .G> o+&`""Bӫ++@5*4"/dHI""++''575575462+KJ*@5u  u5*ku  ukU2#5!#335"&462#3+++<4&&4&k3#@@@&4&&4@? %&&'77'7 && ##qp?{+?''77'&&'7''7'7FPj#K- &EZ;>pQR{;"q6Z.0++%''575575462+KJ*5u  u5*ku  uk+U3%!2#!"&54635#535#"&554633533#32##V+U@ *+U@ +* @ * @ +U &%'7''763#!"&552654ᕗ!2"LFZ!![GLVW:TT:W1F"UUUU@@37!!3!535'5!&Gkkk*j++j++@75!5#72####"&55++++2##3@++@@k@@#33#@ -8CN7!'#264&"264&"%##"&55!##"&5576332&"&54677"&54677"&54677k* ),   , Y X `À  *  %   %   % +U !5##5#3'5#3#35#573#5##35!U+U@**@+@V@*k++@++@@@VV@@+7!2654&''!##"&' m4& @+UVV&:H+D{@+17="26447&546325462632#"'"&55#"&"&52463,, ,   ,  PpPppPp ,,F!!   !!  qOqOOqOqU@ 5264&"5#%"&55##46332322655#"&547'7w  k&,   "-+   jj;kU` $-+#+$2"&43!2##33!"&5477'#2"&4Z""F< L M+o""""o* #+ 5""@@ %5#5##33572#!"&5463UVUUVjVUUVUU*U2#5!#335"&462#3+++<4&&4&k3#@@@&4&&4U+'-6264&""2646"26472#!"&54637"jKKjK  3  Dx2FUKjKKj    7VyG2@"&46263"44&&4&@PpoQQopU&4&&4qKLL@+ '6265#"&5#6"342#!"&54633462X?+&4&+4&U*?X?>,&&,&,??,U@ +5#5#5#'5#5#5#3#5##5##33533++++++++++++++++++@++U**V++++U**V++++++++++++264&""''&5546332ht $  ku $  @264&##72###E@5KK5@U"VKjK@@ %5#5##3357!57'5!7U@*@@*++++2+@@+@@*++*VC@@7632#"&5463320]/ $( K  ]0/  K Ֆ ($ @+6264&"&264&"&2""@"""M""""@UU+U &%'7''763#!"&552654ᕗ!2"LFZ!![GLVW:TT:W1F"UUUU+U5'72#!"&5463U+kk+j+@ !5264&"5#72#!5#5463   +&UU&UU  jj&UU&::''7&6766'&47= L. @Z @ .MZF+U#6264&"332#!"&54633462"X??X?+'DD#(8((8?X??X*8((8(U )$264&"7#3264&"%#"&5#"&5#5463!r@5_K@+&4&&4&++u5Uk&&&&V@@ 1!'#264&"264&"%##"&55!##"&5576335332k* ),   ,55`€  ++@"%54&"6"26472##'#"&5463XPX0""0"[U@@U%%"0!!0h@@+@@%5'2'"54777@ xr xrk-( )-, B)-,'6264&"%3##5&&'#53667532"&4|WW|WT,,`C*C`,,`C*C`F22F2kW|WW|S*C`,,`C*C`,,`2F22F`@''z@@k+ 7!!3264&".5462k*j"@ +GKjKU*;""++u%%4|+5KKk+264&"&2.54,, |W,+4'  ,,W>PF=EAR>++ %5##376''&%2#!463u+`5&+U++&U::''7&6766'&47= L. @Z @ .MZF@@ !7!'''265##526572#!"&5463k*`J6J>W+>,&`@ X>,?kA'**@U%5#%##5##5#57!'!5@+UVUUU+kk++!77'+*`"<QU@ I2654&"264&#"264&#"73##"&55&&535&&535&&535463323$$$$$@$ $@$@$@ @$@""""-, ,-, ,>"7'77#5726323"'#5'6"&462ӕi"'*o ?G. -+-I""c+Id/"3+5@**""k+ 5#5##335&2.54U@*@@*S|W,+4' +*@@*@@W>PF=EAR>k+ 6''&7'62.54>PGG|W,+4' _PGGW>PF=EAR>@@#''588k+"6274&"6"2654&2.54n;4;f""i|W,+4' .DW>PF=EAR>@@ %#7'7#57'53'73''71>=π1=>O1>=π1=>1=>O1>=π1=>O1>=@+463#5#'53#5&&553353UA*56j*."5".+*+7Vk"11"U@ 777#536264&"7"&55##46332322655#"&547'7U+U+  ;,   "-kK  /kU` $-@@ &54633462"632##54)  ?X??XH4@!9) *NX??X?G' u++"&.6%54&#"337335'26!467623#462"6462"B>;E!$ < !+.V.++    &&! !.D--Dk3    U@ 0$264&"'35#5#264&"2##'##57"&554>R+kk*kSX, +*Q*0 ,55UUUU-"3+ ++ +$k+ *%5#264&"7#3#'##57&&554677#53#k] +*Q*0" D;fF?Akk`& ** "$) *+!(/3735'735"&''&#"3576"&46255#573#'7{;-%.+, -G1  p*&a""55u5uu55Bӫ+,@7*".dGI""%56& Z %56 UU #%53'53'5373#53#535335353UVVVV*VVVVVV*VVUVVVVVVVVVVVVVVVVVVUU!'7ww*xxk+3k+k++ %7#&2"&4U}}}V}}}k@77kkkkUU'7!5!'wwx*x++ %'7''72"&4kMMMMMMMM}}}MMMMMMMM@}}}Ik77'7wwI'7IbbbbbU'7'Հbbbbkk ''7'77wwwwwwwwwwwwwwwwwU''bbUbbI'7bbIbkk 3#5#53#5'53#3#5+j*@@*jj@@jj@@j*j*@V@*jkk 3#5353#'53#553#5U@j**j@*jj*U*jj*@@j**j@@ !!5!5!@+j**k++U+2"&462"&4&2"&4""""""+""""""U+62"&462"&46"&462""""<""""""D""VU7#7&#"32673#"&4632y2E&45KK5*B ,\;FddFGy2E&KjK/&8HddUb '777'bbbDDbbDDbbDDbbDD@b %7'77'7DbbDDbb|DbbDbbUU7'#Uw*xwwk@ 353#'M+M@MMU@ %'7#33'7M+MMMUU'737w*xww 3#%'7++ bbw 3#'7'7U++bbk'264&"264&"7!547'76275!"&7  t  ==-1 D 1*W|W@    W-KK--11UU>XX@+!%'73#5'7'753'64'7((<\\zbwwb!V1 )QQ)\\ybwwbd2z4*X*V1+U6264&"72#"&463#53""*GddGFdd***""eedd@kU**++%2654''7&#"62"&4Fe%0r%09FeS}}}UeF90%90%e}}}++%654&#"27'2"&4$eF<-i<-$e}}}-Pp K  //^/ $&+?,>W+pPK K Ֆ ($ /\1/  @@:54&"32##"&5546354622#"&54633276Z k , K  //^/ $  U U   K Ֆ ($ /\1/  C(%"'&'&55&""''&476 %#53#75 .h. 5i" K`5BB5dK `@@ $3#2#"&54633276#5++ K  //^/ $+v K Ֆ ($ /\1/   U+ 5##5##5#72#!"&57+++UVVVVVVU+%5#5#2#!"&57***jjV++@++ 5##5##5#%2#!463k++*++U++++++U++5#5#2#!463***U+UUV++UU%55"&5472'654&#'7UUFeK5FeK5UU@UV@eF2)!5K+eF2)!5K@UV== #)'654'57'567'7#7&5477'6733 &/O2 &/33f 33E0*4&/2,4&/33E0* @U%753756654'553'4677#7&*22H8&/%0U*H8&/%022뀀32F;\, B*5%/++k;\, B*5%/32k %'353#2##"&5463UUU@*VVVjj*+!)2##&'3#&'54632#4&#2#52#4&#k-Aja*qO&@>W+>,+* bOq&X>,?@U -!'#264&"264&"%##"&55!##"&5576332k* ),   ,+`À  @#%#2##"&54635373#53'53U+++k+k+k*U@֫++++%55#3572#!463UUիDDEU /7M75"&55'73#"&54632####3232654754&"32##"&554635462gV+}XY}}Y!+ +  ,/Y j ,A*fAaY}}YX} 6* * @/D  U U  *k&&66!##5#%!532&4&&4&#2&&4&&p*++j*3 k&&667'#5%%70303a0-"1030g((""`(_yB @k 2!5335"&462#3*+<4&&4&k3#֖&4&&4+@%'#"&5533276%33#"&553 OI&K H t&,>* $& e&+?,@@%2##5#"&55332%33#"&553  `&k&,?+&&+?,@@ !33#"&553##57#"&5533232k&UU,?+?`&k* &+?,@U&+@!*%'#"&''&67367'#"&''33&&66Z{ Q##040>c(<***$?#` @~  % ."WU+3'# #U5"*%'#"&55463323"'333#"&5536&462Kl&9>8J&,?+/"TK&{!//O&+?,""+U (5#5#5#7"3#!"&552654ᕗ!2*****VK**`**`**"UUU+!2#!"&54633'77FUUF+VU++FUUF@5!2###5#"&5463UkkU+++@%!2###5#"&5463kk+++@2#5'5463353353VKjK*V*kuK@@KuUUUU+)"&462"&462#5#76332#5#546332#r$$$$@@6  6@ @ $$$$uuU 762&"62'6 &"k>=*,~,+L@ba+PP==+,,+@aa+OOU+ +%5#5##335'354&"2#!"&5546335462U@*@@*W'6'?X?*@@*@@++''F+,??,+U (763&'77&'7!66&%2#"&54776k@\1%  +'1 E3*;O=w- 4o@=% >*'<3;/9)-  @#'663232'354&"'#!"&5547'7';),?t'6','(6?,++'' +U+ +5#535#264&"73#"&5#"&5535#'732k@@UI  *&4&+V@@@@++@@  3*&&@@*@@+73'#''#"&547'#'632'3J+e;6CX}&<VJ6CX}&c*`<&}XC6;&}XC6d++ %5#62"&4k}}}**}}}@+ 3#462"VV$$g$$++ %3#'&&46lLLl+QooLllLVzz++ %667#&73.2"&4:SV@?iS}}}WS:V`R:S3}}}UU462"462"&4632#";V<K$$@&! .. .. !&++ b b  ??  +@#'+/39%#57#55#3#3#5#5#5#5#5#5#5#5#3!3+++V+++++*******++++++++V++U**+*++***V++U++U****V++U++U**k $%2#54'6"2!5466"&462"&462UB8*8B78x4&&4%4&&4&$55,$55$:&4&&4&&4&&4#/62!54%#54&"&462"'64'632'##5#5353PX%8@4&&4&+  &&@+@@+#++++"H&4&&4&D&4&+@@+@@ @@ #,%5#5#5#5#5#5#'5#5#5#73!357***V*******V*****Հ@@++U****V++U++U**++U++U**+*@@++ %$"&'3&"&462"&462264&"2"&4%J; eeeS}}})!!aeee}}}++ %62#66"&462"&462264&"2"&4J; "eeeS}}})!!ieee}}}U+%!5754675462"&53++3--3V++j2J    J2U+!%54&"7!5754675462"&53U.N.++3--3o"V)77)++j2J    J2U+'%'667372635462"&53'!57547'7 -3n$V6++<   J2:9++k) ;++%$"&537!5754675462&'7%#67$UV++3--3*FSG+S+j++j2J    J'V3@g2Wg@U+ $5#335#!5754675462"&535j;;j;++3--3V/&&I&&;++j2J    J2@@#2#75'3##5'7#"&=4633'#kVVkjVVkVkVk@VkkV+U /%2654'###7"3&54633&72#!"&5463373,?-&U!4,?-&U!wD''?,&+?,&+@**k $%2#54'6"2!5466"&462"&462UB8*8B78x4&&4%4&&4&$55,$55$:&4&&4&&4&&4k)1>"264"&462&"264"&46254&#"#54&"%2!546326q"" >,,>,"" >,,>,G$& GHG++`*`+/11u""\,>++>I""\,>++>    `'::'UU62!546"&462luF22F2/&++&Z2F33FU$2!54'3##5#5353"&462 lu@@+@@+F22F2/&++&+@@+@k2F33FUU !62!54662"&4"!54&"264@L??IF22F2XVo&&*@@*3F22F!  &&U7#53##5#5355`*6`VV*VV~ U+UU+U@@ %5##5##5#2#!"&5463k++*++VV֖+*++"*%654&'####32325"&55'2"&4~-;0* * fW}}}0C5V + + @T)fA`v}}}@ #5'7'+ViYRRVQQ@,&%2"&547'#"&46327&5462#"'6$%2%&&&4&&%%% X&4&W &&4&X XU+ 72654'"&54732654&'+; D<(C@KddE,! )k;+,*) 5'4TFddFlR!.-"4++ !)62#&"#6264&"2"&4462"6462"J; #b# eeeS}}}j)!**!Weee}}}0++#6264&"2"&4462"6462"3#eeeS}}}j~Ueee}}}0@ ++ !)6273"&'3264&"2"&4462"6462"b# ;J; #eeeS}}}j*!))!eee}}}0++%-62#67'7'77'7''7'7264&"2"&4J; eeeS}}})!!TCeee}}}++ !$"&'3''777'264&"2"&4%J; -.A.-reeeS}}})!!x----eee}}}@@77''%2#!"&5463L*L*@@2#!"&5463!!***+++6264&"2"&4eeeS}}}Ueee}}}++6264&"2"&462"&4eeeS}}}X??X?Ueee}}}?X??X+@ %7'77#t<VCePPe +@ %'7''%'7'77PG^$$^G%t##t<<0[>VV>[ePPe  DN2#&&''4##32765'2##5#"&53324##5324#"#476327"&'3d E5Qo/$ /5#11 #HQd Dc;`Q9{"Y/ &S    62  Qc<`@+ ##5###5!&2"&4+*+""@뀀+j""++ !53#5!3#'3#k*@@@@@@k++Uj@@@@ %$264&"53#!"&5463!2#"3H5*G+*@@7!54&"64&"2'463!2#!"&5XPX&4&&4*%%t4&&4&++627&&"6"264&2"&4/XNX4&&4&}}}fE&%&4&&4f}}}+%-9733!"&5477'#53367##2"&4&2"&475#53533#M+F$ K%R """"@@*@@+ 5**+ M#H""""@+@@+@++6264&"62"&47'5''7'7|WW|WEpppUeCbbHbUX|WW|qqqp24L%/7GPp/bs!**Pp! W*5>XzL%//qOF7.!S X qO,( >W++ 77'7264&"62"&47'7'7iD|WW|WEppphbbHbjDX|WW|qqq~R RR!S +&2L5##5#7!47'&6632762"&554$2"&5545!##"&55#"&55#"&@V54  AU * =&AB%      KKKK ++%5#75#72#!463***U++UU@%!2#!"&5463#53#535U*jj@j,V*@k+@k+@@@ %5##5##5#2#!"&5463k++*++VV֖+*@@ )5#5#5#"26472#!"&54633662k֖t  Y * @++U**V+++   *@@-%54&""264&"26472#!"&54633662XPX4&&4&7  Y * k%%&4&&4{   *@@ %264&"5#5#2#!"&54633662  ****Y *   ̀U++@*@@$%5#55"26472#!"&54633662UUkk   Y * U@jk@   *@@$%7#5##6"26472#!"&54633662k@V@t  Y * kUU   *@@ #77''6"26472#!"&54633662ի7  Y * 7   *U#'72654''"&54635eFUU5Kq5KeFUU[)2Fe@VU@K5DK5!)2Fe@VUU%3'337#!"&54676632+@kk@Vr):?,5KB0L-:XjjV=*,?K51I'0IU+7572#!"&546356 *Vk@ 2'463k@@Uk@%#72'463kkk/@@UU@75#5#7#3#3#"&'#53&55#53547#5367'7627+VVV-++-<;F;<-++-<#//#++U***++##++*#..# %''&'&&77'61$Q ^@\&X k1 X&\@\ Q$U3'34632&#"%##"'73265#@UV@eF2)!5KV@eF2)!5K@UUFeK UFeK5+U!!VՈ++ 77''62"&4L=}}}L}}}@%#2#!"&54633#53#3#Ֆk@  U+7572#!"&546356 *V+ %7'7''77cccbbb+U5!5!2#!"&5463VVU++Հ+@@ 3#53!53'53Հk@!5373!##"&Jj++U+ 3'5#5##!"&5463vv@@u++U++UV@@/264&"%2#!"&55463264&"%2#!"&55463""@ /""@ @""f ""g Ik77'7ww k 77'77'7 wZxYwwYĈ@@%5!332#!"&54633533#5++kk+*++kk@@ 2#!"&553!!#54637#53'7+*+l77kkUU*UU8*8kk++ %72"&42"&4/QQ(}}}  ѯQU}}}A  ++/2####54&"#"&553264&##54633546232  Q"0"Q !! V,V,V !! Q"0"Q   V++&62654'#"'2"&42"&4&2"&4en@!OS}}} pUeFZP# F}}}S+9%'.5463263250;C2:&&:2C4=E90.D71D--D1'T>>+9-%>54&#"#&&#"2'.5463260.6+ +(+ +6.0`2C;05E=4C2:&&t+,<. ** .<,+ND17D.0>>T'1D--++5#5#2#!463***U+UUV++UU+!6462"'654&"327#!"&54633&4&&4R?X??,^ 4&&4&MR,>>X?_VU6!%'#"'5332673'"#66327#7&cgh(0>,,6-'9+':+T8=,,6hg,,60%$0%6J,,6 @@"&+/48<%5353!!"&553'5353"&53#532##5#46#57#5@+++U+++++U*+*+++++++++*++++U**V+++++**V++ @@ #(,075335375#2##"&554635353"&53'53'53++*+++++++@++++++++*++V**kU 7!!%'353k**U+땕+@ %7'77#t<XX|W@VS@UZ-7pp8,W|WW>VSP+U 7#5#7##5#j@@jVU+ 75#'3!!57'UUUUUUU UKKUUKUUUU+ !!57'UUUUՀUUUUU+'54&"264&"72#!"&5546335462B'6'1""?X?U+''+""+,??,+++5#5#2"&4***C}}}@++@}}}++ 53264&"2"&453*[eeeS}}}*@++eee}}}À@75#53572#!"&553!!#5463U++@*@UVV,VUU4%7"&477Z&L2dd2y^5Z&54L2dd2y@k##"&5546332x]] @k%7'#%##"&5546332ULL ]] kk ++ #+05=%364'#67#'64'#67#'67&''3&47#73&&'&2"&4]HH)= ? d2 R 9 = = PHH R x = }}},8(R,,),,($88$(*,),,,8$(}}}@@3#5'7#53#!"&54633#++Lj+L֕*+@k !!5!%5!%5353'53++++++++k+++V**U++++V**U+'54&"264&"72#!"&5546335462B'6'1""?X?U+''+""+,??,+U+!)%5!2#!"&55463354&"#462"&462'6')?X?Z""U+'',??,+""U+ #+%5!354&"2#!"&5546335462"&462>'6'?X?Z""U+++''F+,??,+""++*%64&#"'&#"264&""''&5546332p[t $  ,[ u $  ++2#!"&5463353#35+*UU+3'5#5##335#!"&5463vv@@*@@*@u*@@*@@*V@U##5#72##535!3#"&5463U@*@UUUU+V++@@3#5'7#53#!"&54633#++Lj+L֕*+ %3'35%5#535#'775#7#+@kk@k@@@kkU@kk@@kk@@k@V@@V@kk@@kk@+U %%7'654&"32772#!"&54632"&4f>8P88(, ,|>(88P8k, ,+U5!5!2#!"&5463VVU++Հ++@ /%54&"2672##5665#"&5##"&5463373+""->+2F2+>-D''UU-G/#33#/G-++@@'%54&""26472#!"&5463353353XPX4&&4&U++%%&4&&4f*++++CM$264&"7''##"5'&'''&774&465'&776677433276'"#&o*  *OB^K%  %%  %^Bk%#2##"&5463#57#5k***k*րV++UU !62!54662"&4"!54&"264@L??IF22F2XVo&&*@@*3F22F!  &&+7!''72#!"&54633!!"&5+K5K+`@`k*+*+@@!3#2#"&54633276@ K  /0]/ $@6 K Ֆ ($ /]0/  + 35#5#26****xvU+Հ[;Z@%!2#!"&5463#5Uj,V*U U #7'#'73``U`8U``U`8c򫫫c@@"&5472654''#5|DppD7W|W7I*:XPppPX:-G>WW>G,M+@ !5264&"5#72#!5#5463   +&UU&UU  jj&UU&++ '5264&"2"&4 `p1eeeS}}}kp9Deee}}}++ ##463!22'#"&55!5k U  U U  V+ @ U +@+!%7777777''''''%5!5!5!@  + V ++U**V+++@  A%5#&''7#5!"2646"2642#!"&554633&546327632m-#@@#-mVt  t  V/&! !&Հ<W WXX|W@VS@UZ-7pp8,W|WW>VSPk+264&"&2.54,, |W,+4'  ,,W>PF=EAR>++ '5264&"2"&4 `p1eeeS}}}kp9Deee}}}@K6264&"'5'#"&462P88P8j j&4:QQtP!8P88P8j j!PtQQ:4&-+?6264&"7''##"''&'''&77&47'&776677633276>,,>,-+ 5V5 +--+ 5V5 +-,>,,> # J8 8 J ## J8 8 J #@@7GO$4'76''&&''&##"'&773327767776''72#!"&54632"&4p % < %  % < % &""4 '(34 '(3*""@2#"'73264&"3'34"&462ppPB3(/>WW|W@VU@""pp(W|WW>UUP?""kz%'73#5'7'75353#53353=((=\\zbwwb@+++*(PP([\zbxwb++++++%#2##"&546353#53#53U+*+UU++++++@)264&5'75373#'!2#!"&5463&&o 5 5 5 @&4&5 5 5 5 A,V*u '7'537#553''7{tt!]]o*V++]]!ttpp********pp" (2#4&"#4''75&5462&2#4&"#4Š+pp+I@@I ,s|W*?X?*aPppPaFI@@IF $$W>,??,> "-9E%53#5&3#5354623#53546253#5&&753#5&&3#535462k+*V++  +*  U*+*U*+  ** ZZWUU ^U **-ZZ ** ZZ ?UU  "-9E%53#5&3#5354623#53546253#5&&753#5&&3#535462k+*V++  +*  U*+*U*+  ** ZZWUU ^U **-ZZ ** ZZ ?UU k+ 353353353#5'53546332***+@@@****@@@@@'3;$2"&462"&4264&"2"&42"&46##"&46332"&462>=ppp_ŠŠ@@}ppp;Š¡}@%!2#!"&5463'''%'#++U5555+Vj,V*66V++++6@@ $(3#2#"&54633276'#5##5++ K  /0]/ $++*@+` K Ֆ ($ /]0/  ++++U#!53"&5472654''#553#53@+ JddJ=KjK>-**+++4XFddFX4&H5KK5H%S+++++Z(2&"'662&"264&"72##"&5463cC88E ,|,Z;""k E88E,,""f  k#'#5&&5326553#536"&5546253K5*5K$C\C1+*4&&4&++6RFFR6/>>/++++&&&+++@77'7353#!"&53546332V*VkUk+++++%%7'5353#!"&553546332!#!"&55uuU+kjUUU@k++++@ "6264&"'3'32#!"''&54633762""@of ( 6 f]""^^ ( +#+$2"&43!2##33!"&5477'#2"&4Z""F< L M+o""""o* #+ 5""++ %5#5#5#'5#5#5#2#!463kk*++++++UU++@++@++++@++@++U4 '7'3''##3lNX,Ux-m(m mOv@@++ %'7''72"&4ZPi))iPZX}}}gE `aEg6}}}@U%5#%##5##5#57!'!5@+UVUUU+kk++Uk !!5!5!5#5UVV*++++**+k "72#54662#54&"&462"&46237BJP4&&4&v,,.05$ 00`&4&&4;, ,@k 5#5353#'U֖U@U@*@@*@Uk@ ##5#3'353U@*@@UU@*UUU++ %#5##'3353'&2"&4u5+5K5+5K-}}}UUKUUKJ}}}52#!"&54633#!#5'3533@U@*@+*+*U@%5#5#2#!"&5463kUU*@  ',049=A%53#5375353535353532#5"&53'535346353'53k*++++***++++++++++++@++++**+U++++U+++++U*++U++U+**U++U@ +5#5#5#'5#5#5#3#5##5##33533++++++++++++++++++@++U**V++++U**V++++++++++3#2'&557#"&57#547763VVU  A   b)@3##"&55477323A   *V+)  b/2'&5667#"&5547763'##"&5547732 j o 1 P1  j o +  i 4 qUq  i 4 @k %53'3#53!5!%5!%5!++++++++**+++++V**U++@@3#5!2#!"&5463353353kk*+++kUU*++++U &&467264&"62"&4@/&8HH8&jKKjK:eee*TB ,\v\, KjKKjeee++5"&4633"&54752654'7"&546752654'>}}}X"$2F2&KjK=.?Wee2>YX}}}  -,#22#!&45KK5/G+`AFeeFG2+ #%3'7#'##''7&'367#53533#SE#`+e+`MBkm(+.*?5]9@@l,Bjk,5(3?+**+P;+ %7''777U1hUU11hUU1@U5!5!5U@U@*@+ 3'''77U1UUh1UUhk@ 2'463k@@Uk@%#72'463kkk/@@U@77''&&55ի7nRRn7VYY+@2#!"&554632#!"&55463 k  U 733#!3@@@+k 3#!533UUU*@U 3#!333Ukkkk+@!!2#!"&554635!+k @+ @@Uk !!5!5!%5!UVVVV*V++**U++Uk !!5!%5!%5353'53VVVVVUUUjVVkUUUUjVVU 3##5353#53#53'53UkkkkkkkkU 3#53!3353kkk뀀U!!5!Ukk+k/2##"&546332##"&5463#2##"&5463 @ @ @     `2"&4264&"62"&'64&&4&X??X?@&4&&4?X??XXHHXXHH+%532'327'#"&547'7&&'#"&'67&&"'632'654&& ?,!&z5+2O7 +.'.O1>?@&3,? !&4XH>, ,7.XH;*>,?+@  A%5#&''7#5!"2646"2642#!"&554633&546327632m-#@@#-mVt  t  V/&! !&Հ<W W>X+++#'2#4>54&"#4264&"2"&453F2@*"*eeeS}}}*2#9!#eee}}}++@k !!5!5!%5!@*V++**U++@K %##5#53533264&"'5'#"&462++++]P88P8j j&4:QQtP!+++++k8P88P8j j!PtQQ:4&@K 3#264&"'5'#"&462kkP88P8j j&4:QQtP!@V8P88P8j j!PtQQ:4&@+5#72###553##5#53##553#5##53++  + u` ` U +  5  + `` `` +55++@ %#5463323#%3#5!#5#k@@j@@V@j@@@@@5@%&''55'!!+R|"j);q  j n2  C+'@&'77'776!! |Rq"1*jX)r xj?! T  +U 732653"&733'3+2F2+KjKk*K``K#22#5KKw``k@ ##3##53#2#353##"&5546;#@++ ` J* @ u   + @ + V @@%5#%2#!"&5463k***++ 77''5#2"&4܏q)}}}Տq)++U}}}ZR$64&""&4653#5!#35DDaDDnXX|VVlUUUUUDaDDaD X|VV|X@UUUUU++/6264&"2"&4"32653#"&5547632#4'&'&eeeS}}}((&(*&!& Ueee}}}:: 0*)  >+6Je|%#&'&54622654&#"'&476632"&54&"'3262#"'&5432"'&'&5462"54&""&76762'&'&&%"'&#"&47632>.!.&6'&[@.L W4Ig'6&&(* =7)  (3 9GdG:R:  02x21 -.l.-,;N,1 1,N> @%!%#!"&5463!2#5Vj,++ $(83'7'3772#5!!#3#535#"&54635#72##"&55463A5455A*+++kk k @&>''>&@Ukk++**+ e,%##"''76323546232'&546254&" j Iq+8P8* , q  j24(88(5Pk+-5%3#"&5467326'4633"'32#5#"&54462",:',>0%&!6*$282@*k$$%0>,':,!& '*)Iuj$$+k %'73#55#5@UUUUUU@*V@UU@*@+#'%5!2#!"&5463353353#5##5##5*++++*++UV+**********++%66737&&'5'&&4670I@hPI0Ph2NN2QooQlH0Pi0HAhPVlVAzz++ %6737&'5'&&467 iO( OiQooQ Ml lM("("zz @U #'+!!53!5353#53#53#53#53753#53#53@ի++*++kjkVU++++++++++++++V******@U !!5!5!%5!@Vj@@@++k/$264&"'#53&&#"3267"&547##"&4633'#53{4&&4&<<!&&!)8>Z=;#9'->>-+L^&4&&4+&4&<+-==-;%0=Z>+*U27!4''7"&5477&ZZ&2dd2y7&^]&H2FGddGF2y+*+3;CK%&##"'&'&667667676363226462"&462"&462"462"r     'SS'  !   ",,,,,,,, * ** (  (Q, ,u,, ,, 6, ,+U %##5#5462&462"U@@*&4&*$$Vjj&&$$@'64''64'2!546462">>#,,# $luV2F22FA>"30%#X $-/&++&F22F2 @@ #'+/3#54&##5325335335353#535353'53533#+&jj,?+*++*++++++++*++++Ujj&+?++++++U++++U++++V**V++*+U"'%'5'#56776332'532"&4#'@@ 8L Q""`6+K @@@ .!{L1 k""6J 3#"'"&547'"'"&46327&5462627&5462L"7 a"a"6L"f"L7a"a6K@@'5#7&&27653"'&4627 J[Ց;,{,+V|,++8888p8:UZ-7j-<,+,xX,+=O7887p8<++ %7'5#&2"&4Z` C}}}:o}}},##"''6363254623546235462354623#$ \#24  ˶  v @@'%27#"&'#53&47#536632&#"3#3#@3'&6J>cKAAKc>J6&'3'B{{Au"&1H8++8H1&")"++")++!%AU%4&##3533#'326'6677#7&'4'#3#"&46327'&#"32672##'#"&54633 )M ::+  U 8T/ !$33$%/U V'$99;+ / F ! * 3H30@@-62"&47'!23'#'#"&547'#"&5477''7"" L l+2L= /^"" ++#=  5c^U+%%264&#"'3'632#"'##!"&5463,??,: U".,,'% 5L?X>1U"(+>, #UV 2'35#'35#'3'5#'#'i~)++++ezU+++)~+k++FzUj+k@3!537377'7''!##"&KJJj.-------S++.----...+++#'#'#5'75#'7#53'735'753737YEc+dE*Ed+cEYYEc+dE*Ed+cE*Ed+cEYYEc+dE*Ed+cEYYEc+dEk /3'#264&"'5#264&"'35#%#"&5#"&5#5463@kV(@UN UU+6&4&u&4&+VwVVwV*j&&&&s'2"''7264&""&462'&"27677]`CC`" <**<G5!D^D"*<*>5@@&&6&&7&474367t2>0s1z11 2=??FbQ8D2 11z1s0>2=??F8+@+5#32##5##"&554633573353#!"&553+VVU+U+Vk**@++@*++*UU@@'7264&"264&"&264&"&264&"264&"2#!"&5463RNN@NN.*@%CKS73"&26723264&#"#&&""&#"3263%"&'&&'&4766767632$462"6462" 4@4!fO  OfO  k _r^!!6I:]![##M0% ++(-$264&"264&"%#"&'##"&547'#53!'2#^+*-),'-/J7GdUE!+%%+,^++9dF++%'''7''7'7'7777--LL--LL--LL--LLU@7!!5#72####"&55UVV+++2##3k+@@k@@#33#U+"&5473462"k. pQbBDf'<8R " %Z ` ` | R `  *Copyright 2015 Google, Inc. All Rights Reserved.Material IconsRegularFontForge 2.0 : Material Icons : 8-2-2016Version 1.011MaterialIcons-Regular2 '( 4latnsize ,latnligah8T)+n-L/1n9=0@ @JKP^Zb2bfhh  !!H@xDr>b ,Ll:Vr*D\t(>Rfz.>N\ht~ ! #" "! !  " !   !   !  %! !  %! ! ! j$  $ "!  $! !# ! !"  ! !" !#  "!  ! ! !" ! !  ! !  !! !i$  $ ! "!!"! "! $ $$ !   " #k $  "  ! ! l $ $  !&   | $ "$ !! &f  ! "" ! ! "![  !   "! %  !   ! !!  !h $       ! "!J !!!j ! !  +  !     "!$  !! -# ! "!# %_& "!" !,"g 7p8\"Bb(B\v,F`z "8Nbv!!&  ""!! !"!! u $!"!!  ! " '! "!! "!"  ! #!! "!! "!!& "$ ! $  !! " " !!& !   !      !  !  !  !  !  !  " !       !& !!& "  !  "! "   ! !!& !  !   " !" " !  "!!  #" |"  " "" "V8b$Hj >\z$@\x.F^v6Lbx 2FXj| .<HT`lx! ! "! % "! !    "!! " "!! " !! "  W! $ ! " $! "  N" $  !&.  ! $ Y ! ! & v ! !   ! !p # ! !!  !  ! !!     #  !! !! Q " "  ! !! &L " o # !    !#  !! !      ! " !! "!P " ""  " !        !   M "   !           % &!O "   !! '      Z"!~ !m !K" nq !}X !/`@b&Fd0H`x*>Rdt !  !"  !  !" !+! ! !*! $&)! "$& !  !"  $  *! !(! &(! !,! $%! ]# \ ! $$ Z! "'! # &! "  !  !"! # # ! ! $ "! [  !   ! "! B  $ !  # "   !  ! "  # !   "    ! # '! ! ^ #>n:Xv*@Vl ! ! ! &!% " " % " " #! #% " " % " " % "  % "   % " '\ ! !)  "!r %   %! ! s %  " & #! " & #! !a # !! #! !/ "' %! % "0%! %! #!(!Z&T*Rz<b8Zz0Nl0Lf4Ld|  6Lbx   & 8 J \ n    $ .-! !%!!  ! ,! !%!!  ! #! !  "! !  &!  ! "!+! !!"'!  ! "!  " !&%!  !  !! ! " k "  !   !l ! !  !w!" &  !! !! !!!  !%!!  !.! "!  .! "#! "!#! u"  %!!   $! !! !x!" #! #!  !  ! " R  $ ! $$ ! !V   !    ! !C !  ) ! "!  !  !  ! ( ! !     1  ! $ !  !  ! !* ! 'U   !  ! 2  ! $S  "t " a $  !    "!` $   !    / "! t  h  $b $ g  #! ! ! ! ! ! !   #!! ! $-!T!",H`x 0@LXdp|  ! %  "  ! !   " $ "   % ! !  !"_ #3 "!0Rr"4DTdt! #" !&" " ! 5  "!&  "!a  !   !D ! $ !&` !! !"4  .! !! !!],\4Tt0<HT^!!  %  ! !2 ! # #!  !! # 4 ! #!!$#!  ! !! 3 ! !1 ! !$ #!  ! %!"  "!6  ! !0  ! !5  ! #"!%!  Fn&Fde& $ !d& $ !c& $ $g&  f& $ "h&  w& !j& !"l& #i & k & !b&!O$Jn Bd<Zv:Vp $<Tl&<Rh|.@P`p 7 # !> "& #; & !! H  ! :  !!!  !" "!n! N!  !&3 !!!I ! J  !"!I ! 6  $ 3 !#!&L <  !p! $$ D &9   !C    ?  &= & " < &   ! !&1  !&  "! ! ,  "!J  0 &  $ !&A  # E   ! F  ''8  ; &   #=  !B  E    $!  !&@  5  o ! M  !%G  &K  4  2  !  !   !$+       #  '"-&!&   # !# /& ("m!" % 6) !*'Pv$Dd ":Pf| ,<JVblv." % !   "!<& &  "!! "=& & " H!'! "! !# !;& &  # ! %  "!8  ! # !P & !m "  #w  '7  !& "  ! " !&x  #!9  !A &    8 9  q&r" :#v"O 76`4Rn.H`v!! " !! !#!! !! !! #! !$ ?! ! !  &! !!  !$  #! %! $ $ !" > $   !$ $ !$  $ $! Q #! %! $/! ]  !"y!.Ld|0  $  #1   $  2  $!!&WBr&Nv8^8Z| @^|0Nl0Lf2H^t  " 4 D T d t ! ' ! !"! ' ! ! ' !  "!! 8 # !!"  !" !6 !! B"   "!E&  "!A"    $  '!?$ !! $D&   ! "!o& !  =!"  !"7 ! !!^    #!! $    &D&  8 # 6 !!   &5     $ !"  #  "!  "!<  $;     ! ! !"  ! $ !& 9  !!& &  $s  v  i & ! & ! &   !  "      t   ! F & ! ! ! ! ! " $ "!C & $:     N    !&   !u R !!"  !3#$4&!:" !!>&" @!@" !$S ! <d" !! " !! p"" & %!B" !  $A "& "H "" " G"",Z 4X|8Tp2FZn,:HT`j "!! "#  "!!!   $#  ! "!! q#  "" # # "!! U !"! " "!! " #  &E!  # J ! !   #  !  !! !  #"T ! #$ !! !`  !"!L !  & d & c &  # "e&  F !C!y $K!!#w"!DM&&IGuD|Dv2`BnFp<d@d6Vv  0 N l  0 L h $ < R h ~   . D X l   * : J Z j z $.8@+ " !  !!  !! #&  ! !! & ! &S!! "!  !R!! "! !!! #& ! ~"!& $ !}"!& $ !L!! "  ! "  Q!! "! !!& &  !& "!   $   !!  ! U!! "! #K!! ! !& "! !! "  !& & !!    " "W!  !&  !!  !!g$ #!  " O!! ! T!! "! !! ! M!! "!!d"#  "! $  !  P!! !!!! "!V!! #  ! r$ ! #h& ! "! !O!!  &g$ #! h& ! "! !#<!!"  !!]  ! $ Y!! !  !T"" "X!! $W!! Z!! # $! _  ! }  "^ ! &     & ! "!^  !P  #" n " ! N !!  &     $! # &  ! "!   ! `   e $ '  { ! !   ! $  b !!#$ !! $ !F ! !%!S "!!  !   $f $ #!O  %!;  V !!y"!&z \ !$H"J!! c"!N" |x!Q'Ic"$&a! !&b!!![&#R! -\ (D`|&:Nbv ,<L\jvf  $!  !!u "!n" " $! j " !r     ! #x "  !t  $u  !   &v  " %! !G %!  l " $ !  "          w " s  !m" "!&k!   %!"%!  Y  X~!I! dp&& e"qoi#&>R`j{  z   #! %Lx.Lh 8Ph2FZn>!  !=!  !!?!  !U !$ !  ! !&  $ U  &| $ " y  "  $ !W   $ !} $ "{   $ "z $ X " $Y " "!  !&{ $ &z    ! $ & $ "!Z "  ! $ $ $  ! [ " "!!~$ &V  &0Tt*>Rfx!    !! !  !!   ! ! !# ! !s   ! @  !%!    "&! ! "&!  "!*!\"!"  "4_  "!  "! $ &'spectral/assets/img/0002755000175000000620000000000013566674120014442 5ustar dilingerstaffspectral/assets/img/icon.png0000644000175000000620000000767213566674120016112 0ustar dilingerstaffPNG  IHDR>asBIT|d pHYs>ZtEXtSoftwarewww.inkscape.org<7IDATx{P[םǿGHy%K2`+ĉ[oivivvf;vɦ&MfؙNb'bCc7SW$$pm@ 3?9_|!Xn;RNV.hPr` |P@ 1t:><U*l)1`hr=Bo e B8НT7x !\H@G)}r˝޼n9/V (<7f0u? .9{s]rۏtx߿`0t3*P.R@<sFbN<#16n7ȂeYE)r"Jb+QBN&W&E*L.*,MT\WXUP92w\ TX:(X(X,34Qgc/((bZiЪ5l0 =c| *mRD>=DC zՖVŐUf VY2lA6:̊Ņ&AvJ*`kNqqLDec=>j2賡"%(/(B\d'&,@ CŚصZ3׋ !HJHKqwal2FӍʔT<^v7V-e^de|Xwk/-#(!m-C 5;xO&(*9WSpdcߟ?ܪf34gbK#0@{υëgOW΀RiXl&p ^~&~D)Qmmƿ_ybK y -#ۺw 7@РPoN}L y -fJQ5HbK`ɺ+SD@l ΢)Jߒ7+x}>o%5_P~%o%k_於XbJ)~uSCe[`88P'X-/  1@e5\j_礦Ȳ,|$0>lj/P(䤈mJOp0`?qGn7B-7&ۗN1k5d=37+v섘q_꺻eS\hiԜ5+6"N-[3탩oϏ|Kx8nَ_| /+ŐvFF3KXx;/,0l|1 +^klKފ/6V^] pӘ~T6V#ґG6owRG{lhv?kB =6lXͤ}fkB6X ],{JbOZ@;7Ⱓ r-?4;n{| =oM  60Ͳ DkV}JٞZO7oԴ/bﺍ,$̋dM0`kVVN;lD68J)L &^]cm,d,J,S L)dNiGC W:`u:002 cB`b`SgNdLp+1 ,(dlƨ׋NڂVҸl1@;?+ '¾ X +qZ-JsPχ˝8ԀKLvVgd]&p-,|IQs}]bU);svTCN.AHbV[R{kqxx~<宰Oq=>6&%?82NA yj5ۀWaQQaiwX1ف??,7h4xt+~WPd1L @8t KvUB̉<m !2˙ c=RԴ M"l+y Ekm5!zu7_.n}6`j}DkǡdY&Je0݂i(J'}_Ŀ{{AeGt"lsqg&Lt䦦#;e2δ sG4KgfDۍ>2!H'"+9+L)JNFvr*)nLt^|AMhL (nRA )Y)O3l S`5 r~UU%Qi>nN73?FAAf 6FFiE:gSV#csB`ILBHU/Ba|b;qWWa1r-/ٴruwΚi41+1 ^ejZP:{qOo_ 3TnLPf禦i 𧔠j;ܑC;q>{bzβ!?־ffKxՉggqRJBt n?}4 + Nl ZoHSih3KKGB4;l^]CxA>0ܘJY[sBZ #ژIi`zuqq(^Yp9cxzH%2WIc,/(b݅dhu|MVr 4SYDKքC֜07@\Yw#\sH[4QQXnJF YaTvg@E>/1pl',3$aﺍLݕ_(yoiCsP,; yt bMTX*DWFOH;F9݅ȉ4Jau^#X fBcl Ayu#h&+]NL*{lkYtT V㛻+~6 RL _Z&Fׂe#܈Rsoj.^-V̉)xTrlS39yl[",s</kԞeZ2 znj1O~4lf900y'xrw+fΚSud L./+;vt+8f-#h$ei)*}FR̗6my%i`r3 | RAYupAx|ӁΝXՅŋ}I5O uxb Ŗ)x  ˔"#LqG?kHl90&pxs 0MRXQx ݝW&`mr]ʷl 0([z#N̈́%'r`&" 0Q =6ۺp+eJFAf ̲)d,g[ݰ k&0ډj"Z>.iz=0&p}7H6 88*_V` @e- . hS[hԩ[8PJ+NHPL08pBl5 s\RjGT HW8&!93"~I(17 `0 ! lFࡄǓ:_Zq|OpY B qgpbNPN{7fMwqQJ_""<~7Y9{p4|kf@5 yi1:^G)^_3߇R@1,L>"Q'`rRzrBst$IENDB`spectral/assets/img/matrix.svg0000644000175000000620000003720513566674120016474 0ustar dilingerstaff spectral/.appveyor.yml0000644000175000000620000000427513566674120015040 0ustar dilingerstaffimage: Visual Studio 2017 environment: DEPLOY_DIR: Spectral-%APPVEYOR_BUILD_VERSION% matrix: - QTDIR: C:\Qt\5.12\msvc2017_64 VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat" PLATFORM: init: - call "%QTDIR%\bin\qtenv2.bat" - set PATH=%PATH%;C:\Qt\Tools\QtCreator\bin - call "%VCVARS%" %platform% - cd /D "%APPVEYOR_BUILD_FOLDER%" before_build: - git submodule update --init --recursive - git clone https://gitlab.matrix.org/matrix-org/olm.git - cd olm - cmake -LA -G "NMake Makefiles JOM" -H. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX="install" -DBUILD_SHARED_LIBS=NO - cmake --build build --target install - cd .. - git clone https://github.com/frankosterfeld/qtkeychain.git - cd qtkeychain - cmake -LA -G "NMake Makefiles JOM" -H. -Bbuild -DCMAKE_CXX_FLAGS="/EHsc /W3" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX="install" -DQTKEYCHAIN_STATIC=ON - cmake --build build --target install - cd .. - git clone https://github.com/commonmark/cmark.git - cd cmark - cmake -LA -G "NMake Makefiles JOM" -H. -Bbuild -DCMAKE_CXX_FLAGS="/EHsc /W3" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="install" -DCMARK_SHARED=ON -DCMARK_STATIC=OFF -DCMARK_TESTS=OFF - cmake --build build --target install - cd .. build_script: - cmake -LA -G "NMake Makefiles JOM" -H. -Bbuild -DCMAKE_CXX_FLAGS="/EHsc /W3" -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX="%DEPLOY_DIR%" -DUSE_INTREE_LIBQMC=1 -DQt5Keychain_DIR="qtkeychain/install/lib/cmake/Qt5Keychain" -DOlm_DIR=olm/install/lib/cmake/Olm -DCMARK_LIBRARY=C:/projects/spectral/cmark/install/lib/cmark.lib -DCMARK_INCLUDE_DIR=C:/projects/spectral/cmark/install/include -DDEPLOY_VERBOSITY=%DEPLOY_VERBOSITY% - cmake --build build after_build: - cmake --build build --target install - windeployqt --release --qmldir qml --qmldir imports "%DEPLOY_DIR%\spectral.exe" - copy C:\projects\spectral\cmark\install\lib\cmark.lib "%DEPLOY_DIR%\" - copy C:\projects\spectral\cmark\install\bin\cmark.dll "%DEPLOY_DIR%\" - 7z a spectral.zip "%DEPLOY_DIR%\*" artifacts: - path: spectral.zip name: portable spectral/spectral_win32.rc0000644000175000000620000000005413566674120015546 0ustar dilingerstaffIDI_ICON1 ICON DISCARDABLE "icons/icon.ico" spectral/res.qrc0000644000175000000620000000716213566674120013670 0ustar dilingerstaff qtquickcontrols2.conf qml/main.qml imports/Spectral/Component/Emoji/EmojiPicker.qml imports/Spectral/Component/Emoji/qmldir imports/Spectral/Component/Timeline/MessageDelegate.qml imports/Spectral/Component/Timeline/qmldir imports/Spectral/Component/Timeline/StateDelegate.qml imports/Spectral/Component/AutoMouseArea.qml imports/Spectral/Component/MaterialIcon.qml imports/Spectral/Component/qmldir imports/Spectral/Effect/ElevationEffect.qml imports/Spectral/Effect/qmldir assets/font/material.ttf assets/img/icon.png imports/Spectral/Setting/Setting.qml imports/Spectral/Font/MaterialFont.qml imports/Spectral/Font/qmldir imports/Spectral/Setting/qmldir imports/Spectral/Panel/qmldir imports/Spectral/Panel/RoomDrawer.qml imports/Spectral/Panel/RoomListPanel.qml imports/Spectral/Panel/RoomPanel.qml imports/Spectral/Panel/RoomHeader.qml imports/Spectral/Component/ScrollHelper.qml imports/Spectral/Component/AutoListView.qml imports/Spectral/Component/AutoTextField.qml imports/Spectral/Panel/RoomPanelInput.qml imports/Spectral/Component/Timeline/SectionDelegate.qml assets/img/matrix.svg imports/Spectral/Effect/RippleEffect.qml imports/Spectral/Effect/CircleMask.qml imports/Spectral/Component/Timeline/ImageDelegate.qml imports/Spectral/Component/Avatar.qml imports/Spectral/Setting/Palette.qml imports/Spectral/Component/Timeline/FileDelegate.qml imports/Spectral/Component/FullScreenImage.qml imports/Spectral/Dialog/qmldir imports/Spectral/Dialog/RoomSettingsDialog.qml imports/Spectral/Dialog/UserDetailDialog.qml imports/Spectral/Dialog/MessageSourceDialog.qml imports/Spectral/Dialog/LoginDialog.qml imports/Spectral/Dialog/CreateRoomDialog.qml imports/Spectral/Dialog/JoinRoomDialog.qml imports/Spectral/Dialog/InviteUserDialog.qml imports/Spectral/Dialog/AcceptInvitationDialog.qml imports/Spectral/Menu/qmldir imports/Spectral/Menu/RoomListContextMenu.qml imports/Spectral/Menu/Timeline/qmldir imports/Spectral/Menu/Timeline/MessageDelegateContextMenu.qml imports/Spectral/Menu/Timeline/FileDelegateContextMenu.qml imports/Spectral/Dialog/FontFamilyDialog.qml imports/Spectral/Dialog/AccountDetailDialog.qml imports/Spectral/Dialog/OpenFileDialog.qml imports/Spectral/Dialog/OpenFolderDialog.qml imports/Spectral/Component/Timeline/VideoDelegate.qml imports/Spectral/Component/AutoRectangle.qml imports/Spectral/Component/Timeline/ReactionDelegate.qml imports/Spectral/Component/Timeline/AudioDelegate.qml spectral/src/0002755000175000000620000000000013566674120013153 5ustar dilingerstaffspectral/src/spectraluser.cpp0000644000175000000620000000015413566674120016371 0ustar dilingerstaff#include "spectraluser.h" QColor SpectralUser::color() { return QColor::fromHslF(hueF(), 0.7, 0.5, 1); } spectral/src/controller.cpp0000644000175000000620000002752213566674120016050 0ustar dilingerstaff#include "controller.h" #include "settings.h" #include "spectralroom.h" #include "spectraluser.h" #include "events/eventcontent.h" #include "events/roommessageevent.h" #include "csapi/account-data.h" #include "csapi/content-repo.h" #include "csapi/joining.h" #include "csapi/logout.h" #include "csapi/profile.h" #include "utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Controller::Controller(QObject* parent) : QObject(parent) { QApplication::setQuitOnLastWindowClosed(false); Connection::setRoomType(); Connection::setUserType(); connect(&m_ncm, &QNetworkConfigurationManager::onlineStateChanged, this, &Controller::isOnlineChanged); QTimer::singleShot(0, this, [=] { invokeLogin(); }); } Controller::~Controller() { for (auto c : m_connections) { c->stopSync(); c->saveState(); } } inline QString accessTokenFileName(const AccountSettings& account) { QString fileName = account.userId(); fileName.replace(':', '_'); return QStandardPaths::writableLocation( QStandardPaths::AppLocalDataLocation) + '/' + fileName; } void Controller::loginWithCredentials(QString serverAddr, QString user, QString pass, QString deviceName) { if (user.isEmpty() || pass.isEmpty()) { return; } if (deviceName.isEmpty()) { deviceName = "Spectral " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture(); } QUrl serverUrl(serverAddr); auto conn = new Connection(this); if (serverUrl.isValid()) { conn->setHomeserver(serverUrl); } conn->connectToServer(user, pass, deviceName, ""); connect(conn, &Connection::connected, [=] { AccountSettings account(conn->userId()); account.setKeepLoggedIn(true); account.clearAccessToken(); // Drop the legacy - just in case account.setHomeserver(conn->homeserver()); account.setDeviceId(conn->deviceId()); account.setDeviceName(deviceName); if (!saveAccessTokenToKeyChain(account, conn->accessToken())) qWarning() << "Couldn't save access token"; account.sync(); addConnection(conn); setConnection(conn); }); connect(conn, &Connection::networkError, [=](QString error, QString, int, int) { emit errorOccured("Network Error", error); }); connect(conn, &Connection::loginError, [=](QString error, QString) { emit errorOccured("Login Failed", error); }); } void Controller::loginWithAccessToken(QString serverAddr, QString user, QString token, QString deviceName) { if (user.isEmpty() || token.isEmpty()) { return; } QUrl serverUrl(serverAddr); auto conn = new Connection(this); if (serverUrl.isValid()) { conn->setHomeserver(serverUrl); } connect(conn, &Connection::connected, [=] { AccountSettings account(conn->userId()); account.setKeepLoggedIn(true); account.clearAccessToken(); // Drop the legacy - just in case account.setHomeserver(conn->homeserver()); account.setDeviceId(conn->deviceId()); account.setDeviceName(deviceName); if (!saveAccessTokenToKeyChain(account, conn->accessToken())) qWarning() << "Couldn't save access token"; account.sync(); addConnection(conn); setConnection(conn); }); connect(conn, &Connection::networkError, [=](QString error, QString, int, int) { emit errorOccured("Network Error", error); }); conn->connectWithToken(user, token, deviceName); } void Controller::logout(Connection* conn) { if (!conn) { qCritical() << "Attempt to logout null connection"; return; } SettingsGroup("Accounts").remove(conn->userId()); QFile(accessTokenFileName(AccountSettings(conn->userId()))).remove(); QKeychain::DeletePasswordJob job(qAppName()); job.setAutoDelete(true); job.setKey(conn->userId()); QEventLoop loop; QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); job.start(); loop.exec(); auto logoutJob = conn->callApi(); connect(logoutJob, &LogoutJob::finished, conn, [=] { conn->stopSync(); emit conn->stateChanged(); emit conn->loggedOut(); if (!m_connections.isEmpty()) setConnection(m_connections[0]); }); connect(logoutJob, &LogoutJob::failure, this, [=] { emit errorOccured("Server-side Logout Failed", logoutJob->errorString()); }); } void Controller::addConnection(Connection* c) { Q_ASSERT_X(c, __FUNCTION__, "Attempt to add a null connection"); m_connections += c; c->setLazyLoading(true); connect(c, &Connection::syncDone, this, [=] { setBusy(false); emit syncDone(); c->sync(30000); c->saveState(); }); connect(c, &Connection::loggedOut, this, [=] { dropConnection(c); }); connect(&m_ncm, &QNetworkConfigurationManager::onlineStateChanged, [=](bool status) { if (!status) { return; } c->stopSync(); c->sync(30000); }); using namespace QMatrixClient; setBusy(true); c->sync(); emit connectionAdded(c); } void Controller::dropConnection(Connection* c) { Q_ASSERT_X(c, __FUNCTION__, "Attempt to drop a null connection"); m_connections.removeOne(c); emit connectionDropped(c); c->deleteLater(); } void Controller::invokeLogin() { using namespace QMatrixClient; const auto accounts = SettingsGroup("Accounts").childGroups(); for (const auto& accountId : accounts) { AccountSettings account{accountId}; if (!account.homeserver().isEmpty()) { auto accessToken = loadAccessTokenFromKeyChain(account); auto c = new Connection(account.homeserver(), this); auto deviceName = account.deviceName(); connect(c, &Connection::connected, this, [=] { c->loadState(); addConnection(c); }); connect(c, &Connection::loginError, [=](QString error, QString) { emit errorOccured("Login Failed", error); logout(c); }); connect(c, &Connection::networkError, [=](QString error, QString, int, int) { emit errorOccured("Network Error", error); }); c->connectWithToken(account.userId(), accessToken, account.deviceId()); } } if (!m_connections.isEmpty()) { setConnection(m_connections[0]); } emit initiated(); } QByteArray Controller::loadAccessTokenFromFile(const AccountSettings& account) { QFile accountTokenFile{accessTokenFileName(account)}; if (accountTokenFile.open(QFile::ReadOnly)) { if (accountTokenFile.size() < 1024) return accountTokenFile.readAll(); qWarning() << "File" << accountTokenFile.fileName() << "is" << accountTokenFile.size() << "bytes long - too long for a token, ignoring it."; } qWarning() << "Could not open access token file" << accountTokenFile.fileName(); return {}; } QByteArray Controller::loadAccessTokenFromKeyChain( const AccountSettings& account) { qDebug() << "Read the access token from the keychain for " << account.userId(); QKeychain::ReadPasswordJob job(qAppName()); job.setAutoDelete(false); job.setKey(account.userId()); QEventLoop loop; QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); job.start(); loop.exec(); if (job.error() == QKeychain::Error::NoError) { return job.binaryData(); } qWarning() << "Could not read the access token from the keychain: " << qPrintable(job.errorString()); // no access token from the keychain, try token file auto accessToken = loadAccessTokenFromFile(account); if (job.error() == QKeychain::Error::EntryNotFound) { if (!accessToken.isEmpty()) { qDebug() << "Migrating the access token from file to the keychain for " << account.userId(); bool removed = false; bool saved = saveAccessTokenToKeyChain(account, accessToken); if (saved) { QFile accountTokenFile{accessTokenFileName(account)}; removed = accountTokenFile.remove(); } if (!(saved && removed)) { qDebug() << "Migrating the access token from the file to the keychain " "failed"; } } } return accessToken; } bool Controller::saveAccessTokenToFile(const AccountSettings& account, const QByteArray& accessToken) { // (Re-)Make a dedicated file for access_token. QFile accountTokenFile{accessTokenFileName(account)}; accountTokenFile.remove(); // Just in case auto fileDir = QFileInfo(accountTokenFile).dir(); if (!((fileDir.exists() || fileDir.mkpath(".")) && accountTokenFile.open(QFile::WriteOnly))) { emit errorOccured("I/O Denied", "Cannot save access token."); } else { accountTokenFile.write(accessToken); return true; } return false; } bool Controller::saveAccessTokenToKeyChain(const AccountSettings& account, const QByteArray& accessToken) { qDebug() << "Save the access token to the keychain for " << account.userId(); QKeychain::WritePasswordJob job(qAppName()); job.setAutoDelete(false); job.setKey(account.userId()); job.setBinaryData(accessToken); QEventLoop loop; QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); job.start(); loop.exec(); if (job.error()) { qWarning() << "Could not save access token to the keychain: " << qPrintable(job.errorString()); return saveAccessTokenToFile(account, accessToken); } return true; } void Controller::joinRoom(Connection* c, const QString& alias) { auto joinRoomJob = c->joinRoom(alias); joinRoomJob->connect(joinRoomJob, &JoinRoomJob::failure, [=] { emit errorOccured("Join Room Failed", joinRoomJob->errorString()); }); } void Controller::createRoom(Connection* c, const QString& name, const QString& topic) { auto createRoomJob = c->createRoom(Connection::PublishRoom, "", name, topic, QStringList()); createRoomJob->connect(createRoomJob, &CreateRoomJob::failure, [=] { emit errorOccured("Create Room Failed", createRoomJob->errorString()); }); } void Controller::createDirectChat(Connection* c, const QString& userID) { auto createRoomJob = c->createDirectChat(userID); createRoomJob->connect(createRoomJob, &CreateRoomJob::failure, [=] { emit errorOccured("Create Direct Chat Failed", createRoomJob->errorString()); }); } void Controller::playAudio(QUrl localFile) { auto player = new QMediaPlayer; player->setMedia(localFile); player->play(); connect(player, &QMediaPlayer::stateChanged, [=] { player->deleteLater(); }); } void Controller::changeAvatar(Connection* conn, QUrl localFile) { auto job = conn->uploadFile(localFile.toLocalFile()); if (isJobRunning(job)) { connect(job, &BaseJob::success, this, [conn, job] { conn->callApi(conn->userId(), job->contentUri()); }); } } void Controller::markAllMessagesAsRead(Connection* conn) { for (auto room : conn->roomMap().values()) { room->markAllMessagesAsRead(); } } spectral/src/emojimodel.h0000644000175000000620000000363113566674120015451 0ustar dilingerstaff#ifndef EMOJIMODEL_H #define EMOJIMODEL_H #include #include #include #include struct Emoji { Emoji(const QString& u, const QString& s) : unicode(u), shortname(s) {} Emoji() {} friend QDataStream& operator<<(QDataStream& arch, const Emoji& object) { arch << object.unicode; arch << object.shortname; return arch; } friend QDataStream& operator>>(QDataStream& arch, Emoji& object) { arch >> object.unicode; arch >> object.shortname; return arch; } QString unicode; QString shortname; Q_GADGET Q_PROPERTY(QString unicode MEMBER unicode) Q_PROPERTY(QString shortname MEMBER shortname) }; Q_DECLARE_METATYPE(Emoji) class EmojiModel : public QObject { Q_OBJECT Q_PROPERTY(QVariantList history READ history NOTIFY historyChanged) Q_PROPERTY(QVariantList people MEMBER people CONSTANT) Q_PROPERTY(QVariantList nature MEMBER nature CONSTANT) Q_PROPERTY(QVariantList food MEMBER food CONSTANT) Q_PROPERTY(QVariantList activity MEMBER activity CONSTANT) Q_PROPERTY(QVariantList travel MEMBER travel CONSTANT) Q_PROPERTY(QVariantList objects MEMBER objects CONSTANT) Q_PROPERTY(QVariantList symbols MEMBER symbols CONSTANT) Q_PROPERTY(QVariantList flags MEMBER flags CONSTANT) public: explicit EmojiModel(QObject* parent = nullptr) : QObject(parent), m_settings(new QSettings()) {} Q_INVOKABLE QVariantList history(); Q_INVOKABLE QVariantList filterModel(const QString& filter); signals: void historyChanged(); public slots: void emojiUsed(QVariant modelData); private: static const QVariantList people; static const QVariantList nature; static const QVariantList food; static const QVariantList activity; static const QVariantList travel; static const QVariantList objects; static const QVariantList symbols; static const QVariantList flags; QSettings* m_settings; }; #endif // EMOJIMODEL_H spectral/src/utils.cpp0000644000175000000620000000002313566674120015010 0ustar dilingerstaff#include "utils.h" spectral/src/spectralroom.h0000644000175000000620000000756113566674120016045 0ustar dilingerstaff#ifndef SpectralRoom_H #define SpectralRoom_H #include "room.h" #include "spectraluser.h" #include #include #include #include #include #include #include #include #include #include using namespace QMatrixClient; class SpectralRoom : public Room { Q_OBJECT Q_PROPERTY(QVariantList usersTyping READ getUsersTyping NOTIFY typingChanged) Q_PROPERTY(QString cachedInput MEMBER m_cachedInput NOTIFY cachedInputChanged) Q_PROPERTY(bool hasFileUploading READ hasFileUploading WRITE setHasFileUploading NOTIFY hasFileUploadingChanged) Q_PROPERTY(int fileUploadingProgress READ fileUploadingProgress NOTIFY fileUploadingProgressChanged) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) public: explicit SpectralRoom(Connection* connection, QString roomId, JoinState joinState = {}); QVariantList getUsersTyping() const; QString lastEvent() const; bool isEventHighlighted(const QMatrixClient::RoomEvent* e) const; QDateTime lastActiveTime() const; bool hasFileUploading() const { return m_hasFileUploading; } void setHasFileUploading(bool value) { if (value == m_hasFileUploading) { return; } m_hasFileUploading = value; emit hasFileUploadingChanged(); } int fileUploadingProgress() const { return m_fileUploadingProgress; } void setFileUploadingProgress(int value) { if (m_fileUploadingProgress == value) { return; } m_fileUploadingProgress = value; emit fileUploadingProgressChanged(); } Q_INVOKABLE int savedTopVisibleIndex() const; Q_INVOKABLE int savedBottomVisibleIndex() const; Q_INVOKABLE void saveViewport(int topIndex, int bottomIndex); Q_INVOKABLE QVariantList getUsers(const QString& keyword) const; Q_INVOKABLE QUrl urlToMxcUrl(QUrl mxcUrl); QString avatarMediaId() const; QString eventToString(const RoomEvent& evt, Qt::TextFormat format = Qt::PlainText, bool removeReply = true) const; private: QString m_cachedInput; QSet highlights; bool m_hasFileUploading = false; int m_fileUploadingProgress = 0; void checkForHighlights(const QMatrixClient::TimelineItem& ti); void onAddNewTimelineEvents(timeline_iter_t from) override; void onAddHistoricalTimelineEvents(rev_iter_t from) override; void onRedaction(const RoomEvent& prevEvent, const RoomEvent& after) override; static QString markdownToHTML(const QString& plaintext); private slots: void countChanged(); signals: void cachedInputChanged(); void busyChanged(); void hasFileUploadingChanged(); void fileUploadingProgressChanged(); void backgroundChanged(); public slots: void uploadFile(const QUrl& url, const QString& body = ""); void acceptInvitation(); void forget(); void sendTypingNotification(bool isTyping); void postArbitaryMessage(const QString& text, MessageEventType type = MessageEventType::Text, const QString& replyEventId = ""); void postPlainMessage(const QString& text, MessageEventType type = MessageEventType::Text, const QString& replyEventId = ""); void postHtmlMessage(const QString& text, const QString& html, MessageEventType type = MessageEventType::Text, const QString& replyEventId = ""); void changeAvatar(QUrl localFile); void addLocalAlias(const QString& alias); void removeLocalAlias(const QString& alias); void toggleReaction(const QString& eventId, const QString& reaction); }; #endif // SpectralRoom_H spectral/src/imageclipboard.cpp0000644000175000000620000000150413566674120016617 0ustar dilingerstaff#include "imageclipboard.h" #include #include #include #include #include ImageClipboard::ImageClipboard(QObject* parent) : QObject(parent), m_clipboard(QGuiApplication::clipboard()) { connect(m_clipboard, &QClipboard::changed, this, &ImageClipboard::imageChanged); } bool ImageClipboard::hasImage() const { return !image().isNull(); } QImage ImageClipboard::image() const { return m_clipboard->image(); } bool ImageClipboard::saveImage(const QUrl& localPath) { if (!localPath.isLocalFile()) return false; auto i = image(); if (i.isNull()) return false; QString path = QFileInfo(localPath.toLocalFile()).absolutePath(); QDir dir; if (!dir.exists(path)) { dir.mkpath(path); } i.save(localPath.toLocalFile()); return true; } spectral/src/main.cpp0000644000175000000620000000532013566674120014601 0ustar dilingerstaff#include #include #include #include #include #include #include "accountlistmodel.h" #include "controller.h" #include "emojimodel.h" #include "imageclipboard.h" #include "matriximageprovider.h" #include "messageeventmodel.h" #include "notifications/manager.h" #include "room.h" #include "roomlistmodel.h" #include "spectralroom.h" #include "spectraluser.h" #include "trayicon.h" #include "userlistmodel.h" #include "csapi/joining.h" #include "csapi/leaving.h" using namespace QMatrixClient; int main(int argc, char* argv[]) { QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QNetworkProxyFactory::setUseSystemConfiguration(true); QApplication app(argc, argv); app.setOrganizationName("ENCOM"); app.setOrganizationDomain("encom.eu.org"); app.setApplicationName("Spectral"); app.setWindowIcon(QIcon(":/assets/img/icon.png")); qmlRegisterType("Spectral", 0, 1, "Controller"); qmlRegisterType("Spectral", 0, 1, "AccountListModel"); qmlRegisterType("Spectral", 0, 1, "RoomListModel"); qmlRegisterType("Spectral", 0, 1, "UserListModel"); qmlRegisterType("Spectral", 0, 1, "MessageEventModel"); qmlRegisterType("Spectral", 0, 1, "EmojiModel"); qmlRegisterType("Spectral", 0, 1, "NotificationsManager"); qmlRegisterType("Spectral", 0, 1, "TrayIcon"); qmlRegisterType("Spectral", 0, 1, "ImageClipboard"); qmlRegisterUncreatableType("Spectral", 0, 1, "RoomMessageEvent", "ENUM"); qmlRegisterUncreatableType("Spectral", 0, 1, "RoomType", "ENUM"); qRegisterMetaType("User*"); qRegisterMetaType("const User*"); qRegisterMetaType("Room*"); qRegisterMetaType("Connection*"); qRegisterMetaType("MessageEventType"); qRegisterMetaType("SpectralRoom*"); qRegisterMetaType("SpectralUser*"); qRegisterMetaType("GetRoomEventsJob*"); qRegisterMetaTypeStreamOperators(); QQmlApplicationEngine engine; engine.addImportPath("qrc:/imports"); MatrixImageProvider* matrixImageProvider = new MatrixImageProvider(); engine.rootContext()->setContextProperty("imageProvider", matrixImageProvider); engine.addImageProvider(QLatin1String("mxc"), matrixImageProvider); engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); if (engine.rootObjects().isEmpty()) return -1; return app.exec(); } spectral/src/roomlistmodel.cpp0000644000175000000620000001745113566674120016556 0ustar dilingerstaff#include "roomlistmodel.h" #include "user.h" #include "utils.h" #include "events/roomevent.h" #include #include #include #include #include RoomListModel::RoomListModel(QObject* parent) : QAbstractListModel(parent) {} RoomListModel::~RoomListModel() {} void RoomListModel::setConnection(Connection* connection) { if (connection == m_connection) return; if (m_connection) m_connection->disconnect(this); if (!connection) { qDebug() << "Removing current connection..."; m_connection = nullptr; beginResetModel(); m_rooms.clear(); endResetModel(); return; } m_connection = connection; for (SpectralRoom* room : m_rooms) room->disconnect(this); connect(connection, &Connection::connected, this, &RoomListModel::doResetModel); connect(connection, &Connection::invitedRoom, this, &RoomListModel::updateRoom); connect(connection, &Connection::joinedRoom, this, &RoomListModel::updateRoom); connect(connection, &Connection::leftRoom, this, &RoomListModel::updateRoom); connect(connection, &Connection::aboutToDeleteRoom, this, &RoomListModel::deleteRoom); connect(connection, &Connection::directChatsListChanged, this, [=](Connection::DirectChatsMap additions, Connection::DirectChatsMap removals) { for (QString roomID : additions.values() + removals.values()) { auto room = connection->room(roomID); if (room) refresh(static_cast(room)); } }); doResetModel(); } void RoomListModel::doResetModel() { beginResetModel(); m_rooms.clear(); for (auto r : m_connection->roomMap()) { doAddRoom(r); } endResetModel(); refreshNotificationCount(); } SpectralRoom* RoomListModel::roomAt(int row) const { return m_rooms.at(row); } void RoomListModel::doAddRoom(Room* r) { if (auto room = static_cast(r)) { m_rooms.append(room); connectRoomSignals(room); emit roomAdded(room); } else { qCritical() << "Attempt to add nullptr to the room list"; Q_ASSERT(false); } } void RoomListModel::connectRoomSignals(SpectralRoom* room) { connect(room, &Room::displaynameChanged, this, [=] { refresh(room); }); connect(room, &Room::unreadMessagesChanged, this, [=] { refresh(room); }); connect(room, &Room::notificationCountChanged, this, [=] { refresh(room); }); connect(room, &Room::avatarChanged, this, [this, room] { refresh(room, {AvatarRole}); }); connect(room, &Room::tagsChanged, this, [=] { refresh(room); }); connect(room, &Room::joinStateChanged, this, [=] { refresh(room); }); connect(room, &Room::addedMessages, this, [=] { refresh(room, {LastEventRole}); }); connect(room, &Room::notificationCountChanged, this, [=] { if (room->notificationCount() == 0) return; if (room->timelineSize() == 0) return; const RoomEvent* lastEvent = room->messageEvents().rbegin()->get(); if (lastEvent->isStateEvent()) return; User* sender = room->user(lastEvent->senderId()); if (sender == room->localUser()) return; emit newMessage(room->id(), lastEvent->id(), room->displayName(), sender->displayname(), room->eventToString(*lastEvent), room->avatar(128)); }); connect(room, &Room::notificationCountChanged, this, &RoomListModel::refreshNotificationCount); } void RoomListModel::refreshNotificationCount() { int count = 0; for (auto room : m_rooms) { count += room->notificationCount(); } m_notificationCount = count; emit notificationCountChanged(); } void RoomListModel::updateRoom(Room* room, Room* prev) { // There are two cases when this method is called: // 1. (prev == nullptr) adding a new room to the room list // 2. (prev != nullptr) accepting/rejecting an invitation or inviting to // the previously left room (in both cases prev has the previous state). if (prev == room) { qCritical() << "RoomListModel::updateRoom: room tried to replace itself"; refresh(static_cast(room)); return; } if (prev && room->id() != prev->id()) { qCritical() << "RoomListModel::updateRoom: attempt to update room" << room->id() << "to" << prev->id(); // That doesn't look right but technically we still can do it. } // Ok, we're through with pre-checks, now for the real thing. auto newRoom = static_cast(room); const auto it = std::find_if( m_rooms.begin(), m_rooms.end(), [=](const SpectralRoom* r) { return r == prev || r == newRoom; }); if (it != m_rooms.end()) { const int row = it - m_rooms.begin(); // There's no guarantee that prev != newRoom if (*it == prev && *it != newRoom) { prev->disconnect(this); m_rooms.replace(row, newRoom); connectRoomSignals(newRoom); } emit dataChanged(index(row), index(row)); } else { beginInsertRows(QModelIndex(), m_rooms.count(), m_rooms.count()); doAddRoom(newRoom); endInsertRows(); } } void RoomListModel::deleteRoom(Room* room) { qDebug() << "Deleting room" << room->id(); const auto it = std::find(m_rooms.begin(), m_rooms.end(), room); if (it == m_rooms.end()) return; // Already deleted, nothing to do qDebug() << "Erasing room" << room->id(); const int row = it - m_rooms.begin(); beginRemoveRows(QModelIndex(), row, row); m_rooms.erase(it); endRemoveRows(); } int RoomListModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_rooms.count(); } QVariant RoomListModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() >= m_rooms.count()) { qDebug() << "UserListModel: something wrong here..."; return QVariant(); } SpectralRoom* room = m_rooms.at(index.row()); if (role == NameRole) return room->displayName(); if (role == AvatarRole) return room->avatarMediaId(); if (role == TopicRole) return room->topic(); if (role == CategoryRole) { if (room->joinState() == JoinState::Invite) return RoomType::Invited; if (room->isFavourite()) return RoomType::Favorite; if (room->isDirectChat()) return RoomType::Direct; if (room->isLowPriority()) return RoomType::Deprioritized; return RoomType::Normal; } if (role == UnreadCountRole) return room->unreadCount(); if (role == NotificationCountRole) return room->notificationCount(); if (role == HighlightCountRole) return room->highlightCount(); if (role == LastEventRole) return room->lastEvent(); if (role == LastActiveTimeRole) return room->lastActiveTime(); if (role == JoinStateRole) { if (!room->successorId().isEmpty()) return QStringLiteral("upgraded"); return toCString(room->joinState()); } if (role == CurrentRoomRole) return QVariant::fromValue(room); return QVariant(); } void RoomListModel::refresh(SpectralRoom* room, const QVector& roles) { const auto it = std::find(m_rooms.begin(), m_rooms.end(), room); if (it == m_rooms.end()) { qCritical() << "Room" << room->id() << "not found in the room list"; return; } const auto idx = index(it - m_rooms.begin()); emit dataChanged(idx, idx, roles); } QHash RoomListModel::roleNames() const { QHash roles; roles[NameRole] = "name"; roles[AvatarRole] = "avatar"; roles[TopicRole] = "topic"; roles[CategoryRole] = "category"; roles[UnreadCountRole] = "unreadCount"; roles[NotificationCountRole] = "notificationCount"; roles[HighlightCountRole] = "highlightCount"; roles[LastEventRole] = "lastEvent"; roles[LastActiveTimeRole] = "lastActiveTime"; roles[JoinStateRole] = "joinState"; roles[CurrentRoomRole] = "currentRoom"; return roles; } spectral/src/emojimodel.cpp0000644000175000000620000042561013566674120016011 0ustar dilingerstaff/* * nheko Copyright (C) 2017 Konstantinos Sideris * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include "emojimodel.h" QVariantList EmojiModel::history() { return m_settings->value("Editor/emojis", QVariantList()).toList(); } QVariantList EmojiModel::filterModel(const QString& filter) { QVariantList result; for (QVariant e : people) { auto emoji = qvariant_cast(e); if (emoji.shortname.startsWith(filter)) { result.append(e); } } for (QVariant e : nature) { auto emoji = qvariant_cast(e); if (emoji.shortname.startsWith(filter)) { result.append(e); } } for (QVariant e : food) { auto emoji = qvariant_cast(e); if (emoji.shortname.startsWith(filter)) { result.append(e); } } for (QVariant e : activity) { auto emoji = qvariant_cast(e); if (emoji.shortname.startsWith(filter)) { result.append(e); } } for (QVariant e : travel) { auto emoji = qvariant_cast(e); if (emoji.shortname.startsWith(filter)) { result.append(e); } } for (QVariant e : objects) { auto emoji = qvariant_cast(e); if (emoji.shortname.startsWith(filter)) { result.append(e); } } for (QVariant e : symbols) { auto emoji = qvariant_cast(e); if (emoji.shortname.startsWith(filter)) { result.append(e); } } for (QVariant e : flags) { auto emoji = qvariant_cast(e); if (emoji.shortname.startsWith(filter)) { result.append(e); } } return result; } void EmojiModel::emojiUsed(QVariant modelData) { QVariantList list = history(); auto it = list.begin(); while (it != list.end()) { if ((*it).value().unicode == modelData.value().unicode) { it = list.erase(it); } else it++; } list.push_front(modelData); m_settings->setValue("Editor/emojis", list); emit historyChanged(); } const QVariantList EmojiModel::people = { QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x80"), ":grinning:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x98\x81"), ":grin:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x98\x82"), ":joy:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa3"), ":rofl:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x83"), ":smiley:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x84"), ":smile:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x85"), ":sweat_smile:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x86"), ":laughing:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x98\x89"), ":wink:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x8a"), ":blush:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x98\x8b"), ":yum:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x8e"), ":sunglasses:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x8d"), ":heart_eyes:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x98"), ":kissing_heart:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x97"), ":kissing:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x99"), ":kissing_smiling_eyes:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x9a"), ":kissing_closed_eyes:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x98\xba"), ":relaxed:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x99\x82"), ":slight_smile:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x97"), ":hugging:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x94"), ":thinking:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x90"), ":neutral_face:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x91"), ":expressionless:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xb6"), ":no_mouth:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x99\x84"), ":rolling_eyes:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x8f"), ":smirk:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xa3"), ":persevere:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x98\xa5"), ":disappointed_relieved:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xae"), ":open_mouth:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x90"), ":zipper_mouth:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xaf"), ":hushed:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xaa"), ":sleepy:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xab"), ":tired_face:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xb4"), ":sleeping:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x8c"), ":relieved:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\xa4\x93"), ":nerd:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x9b"), ":stuck_out_tongue:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x98\x9c"), ":stuck_out_tongue_winking_eye:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x98\x9d"), ":stuck_out_tongue_closed_eyes:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa4"), ":drooling_face:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x92"), ":unamused:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x93"), ":sweat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x94"), ":pensive:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x95"), ":confused:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x99\x83"), ":upside_down:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x91"), ":money_mouth:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xb2"), ":astonished:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x98\xb9"), ":frowning2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x99\x81"), ":slight_frown:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x96"), ":confounded:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x9e"), ":disappointed:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x9f"), ":worried:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xa4"), ":triumph:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x98\xa2"), ":cry:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x98\xad"), ":sob:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xa6"), ":frowning:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xa7"), ":anguished:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xa8"), ":fearful:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xa9"), ":weary:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xac"), ":grimacing:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xb0"), ":cold_sweat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xb1"), ":scream:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xb3"), ":flushed:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xb5"), ":dizzy_face:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x98\xa1"), ":rage:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xa0"), ":angry:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x87"), ":innocent:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa0"), ":cowboy:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa1"), ":clown:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa5"), ":lying_face:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x98\xb7"), ":mask:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x92"), ":thermometer_face:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x95"), ":head_bandage:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa2"), ":nauseated_face:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa7"), ":sneezing_face:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\x88"), ":smiling_imp:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\xbf"), ":imp:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xb9"), ":japanese_ogre:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xba"), ":japanese_goblin:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x80"), ":skull:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xbb"), ":ghost:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xbd"), ":alien:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x96"), ":robot:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\xa9"), ":poop:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xba"), ":smiley_cat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xb8"), ":smile_cat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xb9"), ":joy_cat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xbb"), ":heart_eyes_cat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xbc"), ":smirk_cat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xbd"), ":kissing_cat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x99\x80"), ":scream_cat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xbf"), ":crying_cat_face:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x98\xbe"), ":pouting_cat:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\xa6"), ":boy:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\xa7"), ":girl:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\xa8"), ":man:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xa9"), ":woman:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xb4"), ":older_man:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xb5"), ":older_woman:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\xb6"), ":baby:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xbc"), ":angel:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\xae"), ":cop:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x95\xb5"), ":spy:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x82"), ":guardsman:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xb7"), ":construction_worker:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xb3"), ":man_with_turban:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\xb1"), ":person_with_blond_hair:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x85"), ":santa:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb6"), ":mrs_claus:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xb8"), ":princess:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb4"), ":prince:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xb0"), ":bride_with_veil:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb5"), ":man_in_tuxedo:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb0"), ":pregnant_woman:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xb2"), ":man_with_gua_pi_mao:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x99\x8d"), ":person_frowning:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x99\x8e"), ":person_with_pouting_face:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x99\x85"), ":no_good:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x99\x86"), ":ok_woman:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\x81"), ":information_desk_person:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x99\x8b"), ":raising_hand:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x99\x87"), ":bow:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xa6"), ":face_palm:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb7"), ":shrug:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x86"), ":massage:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x87"), ":haircut:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb6"), ":walking:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x83"), ":runner:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x83"), ":dancer:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xba"), ":man_dancing:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xaf"), ":dancers:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\xa3"), ":speaking_head:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xa4"), ":bust_in_silhouette:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xa5"), ":busts_in_silhouette:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xab"), ":couple:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\xac"), ":two_men_holding_hands:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\xad"), ":two_women_holding_hands:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x8f"), ":couplekiss:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x91"), ":couple_with_heart:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xaa"), ":family:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xaa"), ":muscle:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb3"), ":selfie:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x88"), ":point_left:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x89"), ":point_right:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x98\x9d"), ":point_up:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x86"), ":point_up_2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\x95"), ":middle_finger:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x87"), ":point_down:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9c\x8c"), ":v:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9e"), ":fingers_crossed:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\x96"), ":vulcan:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x98"), ":metal:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x99"), ":call_me:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\x90"), ":hand_splayed:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9c\x8b"), ":raised_hand:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x8c"), ":ok_hand:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x8d"), ":thumbsup:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x8e"), ":thumbsdown:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9c\x8a"), ":fist:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x8a"), ":punch:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9b"), ":left_facing_fist:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9c"), ":right_facing_fist:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9a"), ":raised_back_of_hand:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\x8b"), ":wave:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\x8f"), ":clap:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9c\x8d"), ":writing_hand:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x90"), ":open_hands:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x99\x8c"), ":raised_hands:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x99\x8f"), ":pray:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\x9d"), ":handshake:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x85"), ":nail_care:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\x82"), ":ear:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\x83"), ":nose:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xa3"), ":footprints:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\x80"), ":eyes:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\x81"), ":eye:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x85"), ":tongue:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\x84"), ":lips:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\x8b"), ":kiss:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\xa4"), ":zzz:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x93"), ":eyeglasses:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xb6"), ":dark_sunglasses:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x94"), ":necktie:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x95"), ":shirt:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x96"), ":jeans:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x97"), ":dress:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x98"), ":kimono:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x99"), ":bikini:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x9a"), ":womans_clothes:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x9b"), ":purse:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x9c"), ":handbag:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x9d"), ":pouch:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x92"), ":school_satchel:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x9e"), ":mans_shoe:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x9f"), ":athletic_shoe:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xa0"), ":high_heel:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xa1"), ":sandal:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x91\xa2"), ":boot:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x91"), ":crown:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x92"), ":womans_hat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa9"), ":tophat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x93"), ":mortar_board:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9b\x91"), ":helmet_with_cross:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x84"), ":lipstick:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\x8d"), ":ring:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x82"), ":closed_umbrella:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xbc"), ":briefcase:"}), }; const QVariantList EmojiModel::nature = { QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x99\x88"), ":see_no_evil:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x99\x89"), ":hear_no_evil:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x99\x8a"), ":speak_no_evil:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xa6"), ":sweat_drops:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\xa8"), ":dash:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xb5"), ":monkey_face:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x92"), ":monkey:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8d"), ":gorilla:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\xb6"), ":dog:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\x95"), ":dog2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xa9"), ":poodle:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\xba"), ":wolf:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8a"), ":fox:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\xb1"), ":cat:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\x88"), ":cat2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa6\x81"), ":lion_face:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xaf"), ":tiger:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x85"), ":tiger2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x86"), ":leopard:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xb4"), ":horse:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x8e"), ":racehorse:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8c"), ":deer:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa6\x84"), ":unicorn:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\xae"), ":cow:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\x82"), ":ox:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x83"), ":water_buffalo:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\x84"), ":cow2:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\xb7"), ":pig:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\x96"), ":pig2:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\x97"), ":boar:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xbd"), ":pig_nose:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\x8f"), ":ram:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x91"), ":sheep:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\x90"), ":goat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xaa"), ":dromedary_camel:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xab"), ":camel:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x98"), ":elephant:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8f"), ":rhino:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xad"), ":mouse:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x81"), ":mouse2:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\x80"), ":rat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xb9"), ":hamster:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xb0"), ":rabbit:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x87"), ":rabbit2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xbf"), ":chipmunk:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\xa6\x87"), ":bat:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\xbb"), ":bear:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xa8"), ":koala:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xbc"), ":panda_face:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\xbe"), ":feet:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa6\x83"), ":turkey:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x94"), ":chicken:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x93"), ":rooster:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xa3"), ":hatching_chick:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xa4"), ":baby_chick:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xa5"), ":hatched_chick:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\xa6"), ":bird:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xa7"), ":penguin:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x95\x8a"), ":dove:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa6\x85"), ":eagle:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\xa6\x86"), ":duck:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\xa6\x89"), ":owl:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\xb8"), ":frog:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x8a"), ":crocodile:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xa2"), ":turtle:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8e"), ":lizard:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x8d"), ":snake:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xb2"), ":dragon_face:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x89"), ":dragon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xb3"), ":whale:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x8b"), ":whale2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xac"), ":dolphin:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\x9f"), ":fish:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xa0"), ":tropical_fish:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\xa1"), ":blowfish:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa6\x88"), ":shark:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x99"), ":octopus:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x9a"), ":shell:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\xa6\x80"), ":crab:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa6\x90"), ":shrimp:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa6\x91"), ":squid:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa6\x8b"), ":butterfly:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x8c"), ":snail:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\x9b"), ":bug:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\x9c"), ":ant:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x90\x9d"), ":bee:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x90\x9e"), ":beetle:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xb7"), ":spider:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xb8"), ":spider_web:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa6\x82"), ":scorpion:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x90"), ":bouquet:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb8"), ":cherry_blossom:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb5"), ":rosette:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb9"), ":rose:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x80"), ":wilted_rose:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xba"), ":hibiscus:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbb"), ":sunflower:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbc"), ":blossom:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb7"), ":tulip:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb1"), ":seedling:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb2"), ":evergreen_tree:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb3"), ":deciduous_tree:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb4"), ":palm_tree:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb5"), ":cactus:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbe"), ":ear_of_rice:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbf"), ":herb:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x98\x98"), ":shamrock:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x80"), ":four_leaf_clover:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x81"), ":maple_leaf:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x82"), ":fallen_leaf:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x83"), ":leaves:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x84"), ":mushroom:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb0"), ":chestnut:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8d"), ":earth_africa:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8e"), ":earth_americas:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8f"), ":earth_asia:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x91"), ":new_moon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x92"), ":waxing_crescent_moon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x93"), ":first_quarter_moon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x94"), ":waxing_gibbous_moon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x95"), ":full_moon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x96"), ":waning_gibbous_moon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x97"), ":last_quarter_moon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x98"), ":waning_crescent_moon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x99"), ":crescent_moon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9a"), ":new_moon_with_face:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9b"), ":first_quarter_moon_with_face:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9c"), ":last_quarter_moon_with_face:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x98\x80"), ":sunny:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9d"), ":full_moon_with_face:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9e"), ":sun_with_face:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\xad\x90"), ":star:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x9f"), ":star2:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x98\x81"), ":cloud:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9b\x85"), ":partly_sunny:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9b\x88"), ":thunder_cloud_rain:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa4"), ":white_sun_small_cloud:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa5"), ":white_sun_cloud:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa6"), ":white_sun_rain_cloud:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa7"), ":cloud_rain:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa8"), ":cloud_snow:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa9"), ":cloud_lightning:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xaa"), ":cloud_tornado:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8c\xab"), ":fog:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xac"), ":wind_blowing_face:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x98\x82"), ":umbrella2:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x98\x94"), ":umbrella:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9a\xa1"), ":zap:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9d\x84"), ":snowflake:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x98\x83"), ":snowman2:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\x84"), ":snowman:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x98\x84"), ":comet:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\xa5"), ":fire:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xa7"), ":droplet:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8a"), ":ocean:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x83"), ":jack_o_lantern:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x84"), ":christmas_tree:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9c\xa8"), ":sparkles:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8b"), ":tanabata_tree:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8d"), ":bamboo:"}), }; const QVariantList EmojiModel::food = { QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x87"), ":grapes:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x88"), ":melon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x89"), ":watermelon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8a"), ":tangerine:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8b"), ":lemon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8c"), ":banana:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8d"), ":pineapple:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8e"), ":apple:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x8f"), ":green_apple:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8d\x90"), ":pear:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x91"), ":peach:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x92"), ":cherries:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x93"), ":strawberry:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9d"), ":kiwi:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x85"), ":tomato:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x91"), ":avocado:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x86"), ":eggplant:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x94"), ":potato:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x95"), ":carrot:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8c\xbd"), ":corn:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xb6"), ":hot_pepper:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x92"), ":cucumber:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9c"), ":peanuts:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9e"), ":bread:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x90"), ":croissant:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x96"), ":french_bread:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9e"), ":pancakes:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa7\x80"), ":cheese:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x96"), ":meat_on_bone:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x97"), ":poultry_leg:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x93"), ":bacon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x94"), ":hamburger:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9f"), ":fries:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x95"), ":pizza:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xad"), ":hotdog:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8c\xae"), ":taco:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xaf"), ":burrito:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x99"), ":stuffed_flatbread:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9a"), ":egg:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb3"), ":cooking:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x98"), ":shallow_pan_of_food:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb2"), ":stew:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x97"), ":salad:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbf"), ":popcorn:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb1"), ":bento:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x98"), ":rice_cracker:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x99"), ":rice_ball:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9a"), ":rice:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9b"), ":curry:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9c"), ":ramen:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\x9d"), ":spaghetti:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa0"), ":sweet_potato:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa2"), ":oden:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa3"), ":sushi:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa4"), ":fried_shrimp:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa5"), ":fish_cake:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa1"), ":dango:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa6"), ":icecream:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa7"), ":shaved_ice:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa8"), ":ice_cream:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xa9"), ":doughnut:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xaa"), ":cookie:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x82"), ":birthday:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb0"), ":cake:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xab"), ":chocolate_bar:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xac"), ":candy:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xad"), ":lollipop:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xae"), ":custard:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xaf"), ":honey_pot:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbc"), ":baby_bottle:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\xa5\x9b"), ":milk:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x98\x95"), ":coffee:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb5"), ":tea:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb6"), ":sake:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbe"), ":champagne:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb7"), ":wine_glass:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb8"), ":cocktail:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb9"), ":tropical_drink:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8d\xba"), ":beer:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbb"), ":beers:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x82"), ":champagne_glass:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x83"), ":tumbler_glass:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xbd"), ":fork_knife_plate:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8d\xb4"), ":fork_and_knife:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x84"), ":spoon:"}), }; const QVariantList EmojiModel::activity = { QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\xbe"), ":space_invader:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xb4"), ":levitate:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xba"), ":fencer:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87"), ":horse_racing:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbb"), ":horse_racing_tone1:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbc"), ":horse_racing_tone2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbd"), ":horse_racing_tone3:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbe"), ":horse_racing_tone4:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbf"), ":horse_racing_tone5:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xb7"), ":skier:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x82"), ":snowboarder:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8c"), ":golfer:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84"), ":surfer:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbb"), ":surfer_tone1:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbc"), ":surfer_tone2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbd"), ":surfer_tone3:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbe"), ":surfer_tone4:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbf"), ":surfer_tone5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3"), ":rowboat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbb"), ":rowboat_tone1:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbc"), ":rowboat_tone2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbd"), ":rowboat_tone3:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbe"), ":rowboat_tone4:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbf"), ":rowboat_tone5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a"), ":swimmer:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbb"), ":swimmer_tone1:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbc"), ":swimmer_tone2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbd"), ":swimmer_tone3:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbe"), ":swimmer_tone4:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbf"), ":swimmer_tone5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9b\xb9"), ":basketball_player:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbb"), ":basketball_player_tone1:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbc"), ":basketball_player_tone2:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbd"), ":basketball_player_tone3:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbe"), ":basketball_player_tone4:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xb9\xf0\x9f\x8f\xbf"), ":basketball_player_tone5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b"), ":lifter:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbb"), ":lifter_tone1:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbc"), ":lifter_tone2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbd"), ":lifter_tone3:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbe"), ":lifter_tone4:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbf"), ":lifter_tone5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4"), ":bicyclist:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbb"), ":bicyclist_tone1:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbc"), ":bicyclist_tone2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbd"), ":bicyclist_tone3:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbe"), ":bicyclist_tone4:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbf"), ":bicyclist_tone5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5"), ":mountain_bicyclist:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbb"), ":mountain_bicyclist_tone1:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbc"), ":mountain_bicyclist_tone2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbd"), ":mountain_bicyclist_tone3:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbe"), ":mountain_bicyclist_tone4:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbf"), ":mountain_bicyclist_tone5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8"), ":cartwheel:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbb"), ":cartwheel_tone1:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbc"), ":cartwheel_tone2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbd"), ":cartwheel_tone3:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbe"), ":cartwheel_tone4:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbf"), ":cartwheel_tone5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc"), ":wrestlers:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc\xf0\x9f\x8f\xbb"), ":wrestlers_tone1:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc\xf0\x9f\x8f\xbc"), ":wrestlers_tone2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc\xf0\x9f\x8f\xbd"), ":wrestlers_tone3:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc\xf0\x9f\x8f\xbe"), ":wrestlers_tone4:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbc\xf0\x9f\x8f\xbf"), ":wrestlers_tone5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd"), ":water_polo:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbb"), ":water_polo_tone1:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbc"), ":water_polo_tone2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbd"), ":water_polo_tone3:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbe"), ":water_polo_tone4:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbf"), ":water_polo_tone5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe"), ":handball:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbb"), ":handball_tone1:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbc"), ":handball_tone2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbd"), ":handball_tone3:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbe"), ":handball_tone4:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbf"), ":handball_tone5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9"), ":juggling:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbb"), ":juggling_tone1:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbc"), ":juggling_tone2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbd"), ":juggling_tone3:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbe"), ":juggling_tone4:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbf"), ":juggling_tone5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xaa"), ":circus_tent:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xad"), ":performing_arts:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa8"), ":art:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb0"), ":slot_machine:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9b\x80"), ":bath:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbb"), ":bath_tone1:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbc"), ":bath_tone2:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbd"), ":bath_tone3:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbe"), ":bath_tone4:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbf"), ":bath_tone5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x97"), ":reminder_ribbon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9f"), ":tickets:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xab"), ":ticket:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x96"), ":military_medal:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x86"), ":trophy:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x85"), ":medal:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x87"), ":first_place:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x88"), ":second_place:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x89"), ":third_place:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9a\xbd"), ":soccer:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9a\xbe"), ":baseball:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x80"), ":basketball:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x90"), ":volleyball:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x88"), ":football:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x89"), ":rugby_football:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbe"), ":tennis:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb1"), ":8ball:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb3"), ":bowling:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8f"), ":cricket:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x91"), ":field_hockey:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x92"), ":hockey:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x93"), ":ping_pong:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb8"), ":badminton:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8a"), ":boxing_glove:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\xa5\x8b"), ":martial_arts_uniform:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\xa5\x85"), ":goal:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8e\xaf"), ":dart:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xb3"), ":golf:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9b\xb8"), ":ice_skate:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa3"), ":fishing_pole_and_fish:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbd"), ":running_shirt_with_sash:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbf"), ":ski:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xae"), ":video_game:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb2"), ":game_die:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbc"), ":musical_score:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa4"), ":microphone:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa7"), ":headphones:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb7"), ":saxophone:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb8"), ":guitar:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb9"), ":musical_keyboard:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xba"), ":trumpet:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xbb"), ":violin:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\xa5\x81"), ":drum:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xac"), ":clapper:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb9"), ":bow_and_arrow:"}), }; const QVariantList EmojiModel::travel = { QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8e"), ":race_car:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x8d"), ":motorcycle:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\xbe"), ":japan:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x94"), ":mountain_snow:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xb0"), ":mountain:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8b"), ":volcano:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\xbb"), ":mount_fuji:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x95"), ":camping:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x96"), ":beach:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9c"), ":desert:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9d"), ":island:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9e"), ":park:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9f"), ":stadium:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9b"), ":classical_building:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x97"), ":construction_site:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x98"), ":homes:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x99"), ":cityscape:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x9a"), ":house_abandoned:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa0"), ":house:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa1"), ":house_with_garden:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa2"), ":office:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa3"), ":post_office:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa4"), ":european_post_office:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa5"), ":hospital:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa6"), ":bank:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa8"), ":hotel:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa9"), ":love_hotel:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xaa"), ":convenience_store:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xab"), ":school:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xac"), ":department_store:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xad"), ":factory:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xaf"), ":japanese_castle:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb0"), ":european_castle:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x92"), ":wedding:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\xbc"), ":tokyo_tower:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\xbd"), ":statue_of_liberty:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xaa"), ":church:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x8c"), ":mosque:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x8d"), ":synagogue:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9b\xa9"), ":shinto_shrine:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x8b"), ":kaaba:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xb2"), ":fountain:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xba"), ":tent:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x81"), ":foggy:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x83"), ":night_with_stars:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8c\x84"), ":sunrise_over_mountains:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x85"), ":sunrise:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x86"), ":city_dusk:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x87"), ":city_sunset:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x89"), ":bridge_at_night:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x8c"), ":milky_way:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa0"), ":carousel_horse:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa1"), ":ferris_wheel:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa2"), ":roller_coaster:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x82"), ":steam_locomotive:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x83"), ":railway_car:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x84"), ":bullettrain_side:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x85"), ":bullettrain_front:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x86"), ":train2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x87"), ":metro:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x88"), ":light_rail:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x89"), ":station:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8a"), ":tram:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9d"), ":monorail:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9e"), ":mountain_railway:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8b"), ":train:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8c"), ":bus:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8d"), ":oncoming_bus:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8e"), ":trolleybus:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x90"), ":minibus:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x91"), ":ambulance:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x92"), ":fire_engine:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x93"), ":police_car:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x94"), ":oncoming_police_car:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9a\x95"), ":taxi:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x96"), ":oncoming_taxi:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x97"), ":red_car:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x98"), ":oncoming_automobile:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x99"), ":blue_car:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9a"), ":truck:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9b"), ":articulated_lorry:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9c"), ":tractor:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb2"), ":bike:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb4"), ":scooter:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb5"), ":motor_scooter:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x8f"), ":busstop:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa3"), ":motorway:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa4"), ":railway_track:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xbd"), ":fuelpump:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa8"), ":rotating_light:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa5"), ":traffic_light:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa6"), ":vertical_traffic_light:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa7"), ":construction:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9a\x93"), ":anchor:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xb5"), ":sailboat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb6"), ":canoe:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa4"), ":speedboat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb3"), ":cruise_ship:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\xb4"), ":ferry:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa5"), ":motorboat:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa2"), ":ship:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9c\x88"), ":airplane:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa9"), ":airplane_small:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\xab"), ":airplane_departure:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\xac"), ":airplane_arriving:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\xba"), ":seat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x81"), ":helicopter:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x9f"), ":suspension_railway:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa0"), ":mountain_cableway:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa1"), ":aerial_tramway:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\x80"), ":rocket:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\xb0"), ":satellite_orbital:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa0"), ":stars:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x88"), ":rainbow:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x86"), ":fireworks:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x87"), ":sparkler:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x91"), ":rice_scene:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\x81"), ":checkered_flag:"}), }; const QVariantList EmojiModel::objects = { QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x98\xa0"), ":skull_crossbones:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x8c"), ":love_letter:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\xa3"), ":bomb:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x95\xb3"), ":hole:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8d"), ":shopping_bags:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xbf"), ":prayer_beads:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\x8e"), ":gem:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xaa"), ":knife:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xba"), ":amphora:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x97\xba"), ":map:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x88"), ":barber:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\xbc"), ":frame_photo:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8e"), ":bellhop:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9a\xaa"), ":door:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8c"), ":sleeping_accommodation:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8f"), ":bed:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\x8b"), ":couch:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbd"), ":toilet:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbf"), ":shower:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\x81"), ":bathtub:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x8c\x9b"), ":hourglass:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x8f\xb3"), ":hourglass_flowing_sand:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x8c\x9a"), ":watch:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x8f\xb0"), ":alarm_clock:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x8f\xb1"), ":stopwatch:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x8f\xb2"), ":timer:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xb0"), ":clock:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\xa1"), ":thermometer:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9b\xb1"), ":beach_umbrella:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x88"), ":balloon:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8e\x89"), ":tada:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8a"), ":confetti_ball:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8e"), ":dolls:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8f"), ":flags:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x90"), ":wind_chime:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x80"), ":ribbon:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8e\x81"), ":gift:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xb9"), ":joystick:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xaf"), ":postal_horn:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x99"), ":microphone2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9a"), ":level_slider:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9b"), ":control_knobs:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xbb"), ":radio:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xb1"), ":iphone:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xb2"), ":calling:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x98\x8e"), ":telephone:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x9e"), ":telephone_receiver:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x9f"), ":pager:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x93\xa0"), ":fax:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x8b"), ":battery:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x8c"), ":electric_plug:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xbb"), ":computer:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\xa5"), ":desktop:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\xa8"), ":printer:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x8c\xa8"), ":keyboard:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\xb1"), ":mouse_three_button:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\xb2"), ":trackball:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xbd"), ":minidisc:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xbe"), ":floppy_disk:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\xbf"), ":cd:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x93\x80"), ":dvd:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa5"), ":movie_camera:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x9e"), ":film_frames:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xbd"), ":projector:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x93\xba"), ":tv:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xb7"), ":camera:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xb8"), ":camera_with_flash:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xb9"), ":video_camera:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x93\xbc"), ":vhs:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\x8d"), ":mag:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x8e"), ":mag_right:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xac"), ":microscope:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xad"), ":telescope:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xa1"), ":satellite:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xaf"), ":candle:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\xa1"), ":bulb:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xa6"), ":flashlight:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xae"), ":izakaya_lantern:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x93\x94"), ":notebook_with_decorative_cover:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x95"), ":closed_book:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x93\x96"), ":book:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x97"), ":green_book:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x98"), ":blue_book:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x99"), ":orange_book:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x9a"), ":books:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x93"), ":notebook:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x92"), ":ledger:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x83"), ":page_with_curl:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x9c"), ":scroll:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x84"), ":page_facing_up:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xb0"), ":newspaper:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\x9e"), ":newspaper2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x91"), ":bookmark_tabs:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x96"), ":bookmark:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb7"), ":label:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xb0"), ":moneybag:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\xb4"), ":yen:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xb5"), ":dollar:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\xb6"), ":euro:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xb7"), ":pound:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xb8"), ":money_with_wings:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xb3"), ":credit_card:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9c\x89"), ":envelope:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xa7"), ":e-mail:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xa8"), ":incoming_envelope:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xa9"), ":envelope_with_arrow:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xa4"), ":outbox_tray:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xa5"), ":inbox_tray:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xa6"), ":package:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xab"), ":mailbox:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xaa"), ":mailbox_closed:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xac"), ":mailbox_with_mail:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xad"), ":mailbox_with_no_mail:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xae"), ":postbox:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\xb3"), ":ballot_box:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9c\x8f"), ":pencil2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9c\x92"), ":black_nib:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\x8b"), ":pen_fountain:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\x8a"), ":pen_ballpoint:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\x8c"), ":paintbrush:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\x8d"), ":crayon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x9d"), ":pencil:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x81"), ":file_folder:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x82"), ":open_file_folder:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\x82"), ":dividers:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x93\x85"), ":date:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x86"), ":calendar:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\x92"), ":notepad_spiral:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\x93"), ":calendar_spiral:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x87"), ":card_index:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x93\x88"), ":chart_with_upwards_trend:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x93\x89"), ":chart_with_downwards_trend:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x8a"), ":bar_chart:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x8b"), ":clipboard:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x8c"), ":pushpin:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x8d"), ":round_pushpin:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x8e"), ":paperclip:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\x87"), ":paperclips:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x8f"), ":straight_ruler:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x90"), ":triangular_ruler:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9c\x82"), ":scissors:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\x83"), ":card_box:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\x84"), ":file_cabinet:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\x91"), ":wastebasket:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\x92"), ":lock:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x93"), ":unlock:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x8f"), ":lock_with_ink_pen:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x90"), ":closed_lock_with_key:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\x91"), ":key:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x97\x9d"), ":key2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xa8"), ":hammer:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\x8f"), ":pick:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9a\x92"), ":hammer_pick:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa0"), ":tools:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\xa1"), ":dagger:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9a\x94"), ":crossed_swords:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\xab"), ":gun:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa1"), ":shield:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xa7"), ":wrench:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xa9"), ":nut_and_bolt:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9a\x99"), ":gear:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\x9c"), ":compression:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9a\x97"), ":alembic:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9a\x96"), ":scales:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\x97"), ":link:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\x93"), ":chains:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x89"), ":syringe:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\x8a"), ":pill:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xac"), ":smoking:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9a\xb0"), ":coffin:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9a\xb1"), ":urn:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\xbf"), ":moyai:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9b\xa2"), ":oil:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xae"), ":crystal_ball:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\x92"), ":shopping_cart:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9a\xa9"), ":triangular_flag_on_post:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\x8c"), ":crossed_flags:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb4"), ":flag_black:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3"), ":flag_white:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8f\xb3\xf0\x9f\x8c\x88"), ":rainbow_flag:"}), }; const QVariantList EmojiModel::symbols = { QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x91\x81\xf0\x9f\x97\xa8"), ":eye_in_speech_bubble:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x98"), ":cupid:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9d\xa4"), ":heart:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x93"), ":heartbeat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x94"), ":broken_heart:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x95"), ":two_hearts:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x96"), ":sparkling_heart:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x97"), ":heartpulse:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x99"), ":blue_heart:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x9a"), ":green_heart:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x9b"), ":yellow_heart:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x9c"), ":purple_heart:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x96\xa4"), ":black_heart:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x9d"), ":gift_heart:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x9e"), ":revolving_hearts:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\x9f"), ":heart_decoration:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9d\xa3"), ":heart_exclamation:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xa2"), ":anger:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\xa5"), ":boom:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xab"), ":dizzy:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xac"), ":speech_balloon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\xa8"), ":speech_left:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x97\xaf"), ":anger_right:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xad"), ":thought_balloon:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xae"), ":white_flower:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x90"), ":globe_with_meridians:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x99\xa8"), ":hotsprings:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\x91"), ":octagonal_sign:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x9b"), ":clock12:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xa7"), ":clock1230:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x90"), ":clock1:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x9c"), ":clock130:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x91"), ":clock2:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x9d"), ":clock230:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x92"), ":clock3:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x9e"), ":clock330:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x93"), ":clock4:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x9f"), ":clock430:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x94"), ":clock5:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xa0"), ":clock530:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x95"), ":clock6:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xa1"), ":clock630:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x96"), ":clock7:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xa2"), ":clock730:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x97"), ":clock8:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xa3"), ":clock830:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x98"), ":clock9:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xa4"), ":clock930:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x99"), ":clock10:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xa5"), ":clock1030:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x9a"), ":clock11:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\xa6"), ":clock1130:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8c\x80"), ":cyclone:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\xa0"), ":spades:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\xa5"), ":hearts:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\xa6"), ":diamonds:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\xa3"), ":clubs:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x83\x8f"), ":black_joker:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x80\x84"), ":mahjong:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb4"), ":flower_playing_cards:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\x87"), ":mute:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x88"), ":speaker:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x89"), ":sound:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x8a"), ":loud_sound:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xa2"), ":loudspeaker:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x93\xa3"), ":mega:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\x94"), ":bell:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x95"), ":no_bell:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb5"), ":musical_note:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xb6"), ":notes:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xb9"), ":chart:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xb1"), ":currency_exchange:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x92\xb2"), ":heavy_dollar_sign:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x8f\xa7"), ":atm:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9a\xae"), ":put_litter_in_its_place:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb0"), ":potable_water:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x99\xbf"), ":wheelchair:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb9"), ":mens:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xba"), ":womens:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbb"), ":restroom:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbc"), ":baby_symbol:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x9a\xbe"), ":wc:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\x82"), ":passport_control:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\x83"), ":customs:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\x84"), ":baggage_claim:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\x85"), ":left_luggage:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9a\xa0"), ":warning:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb8"), ":children_crossing:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9b\x94"), ":no_entry:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xab"), ":no_entry_sign:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb3"), ":no_bicycles:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xad"), ":no_smoking:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xaf"), ":do_not_litter:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb1"), ":non-potable_water:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9a\xb7"), ":no_pedestrians:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xb5"), ":no_mobile_phones:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x9e"), ":underage:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x98\xa2"), ":radioactive:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x98\xa3"), ":biohazard:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\xac\x86"), ":arrow_up:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x86\x97"), ":arrow_upper_right:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9e\xa1"), ":arrow_right:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x86\x98"), ":arrow_lower_right:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\xac\x87"), ":arrow_down:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x86\x99"), ":arrow_lower_left:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\xac\x85"), ":arrow_left:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x86\x96"), ":arrow_upper_left:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x86\x95"), ":arrow_up_down:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x86\x94"), ":left_right_arrow:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x86\xa9"), ":leftwards_arrow_with_hook:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x86\xaa"), ":arrow_right_hook:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\xa4\xb4"), ":arrow_heading_up:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\xa4\xb5"), ":arrow_heading_down:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x83"), ":arrows_clockwise:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\x84"), ":arrows_counterclockwise:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\x99"), ":back:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\x9a"), ":end:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\x9b"), ":on:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\x9c"), ":soon:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\x9d"), ":top:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x9b\x90"), ":place_of_worship:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9a\x9b"), ":atom:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x89"), ":om_symbol:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9c\xa1"), ":star_of_david:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x98\xb8"), ":wheel_of_dharma:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x98\xaf"), ":yin_yang:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9c\x9d"), ":cross:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x98\xa6"), ":orthodox_cross:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x98\xaa"), ":star_and_crescent:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x98\xae"), ":peace:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x95\x8e"), ":menorah:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xaf"), ":six_pointed_star:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\x88"), ":aries:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\x89"), ":taurus:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\x8a"), ":gemini:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\x8b"), ":cancer:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\x8c"), ":leo:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\x8d"), ":virgo:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\x8e"), ":libra:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\x8f"), ":scorpius:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x99\x90"), ":sagittarius:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x99\x91"), ":capricorn:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\x92"), ":aquarius:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\x93"), ":pisces:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9b\x8e"), ":ophiuchus:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\x80"), ":twisted_rightwards_arrows:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x81"), ":repeat:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x82"), ":repeat_one:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x96\xb6"), ":arrow_forward:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x8f\xa9"), ":fast_forward:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x8f\xad"), ":track_next:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x8f\xaf"), ":play_pause:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x97\x80"), ":arrow_backward:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x8f\xaa"), ":rewind:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x8f\xae"), ":track_previous:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xbc"), ":arrow_up_small:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x8f\xab"), ":arrow_double_up:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xbd"), ":arrow_down_small:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x8f\xac"), ":arrow_double_down:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x8f\xb8"), ":pause_button:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x8f\xb9"), ":stop_button:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x8f\xba"), ":record_button:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x8f\x8f"), ":eject:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x8e\xa6"), ":cinema:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x85"), ":low_brightness:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x86"), ":high_brightness:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xb6"), ":signal_strength:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xb3"), ":vibration_mode:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\xb4"), ":mobile_phone_off:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x99\xbb"), ":recycle:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x93\x9b"), ":name_badge:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9a\x9c"), ":fleur-de-lis:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xb0"), ":beginner:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xb1"), ":trident:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\xad\x95"), ":o:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9c\x85"), ":white_check_mark:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x98\x91"), ":ballot_box_with_check:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9c\x94"), ":heavy_check_mark:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9c\x96"), ":heavy_multiplication_x:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9d\x8c"), ":x:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9d\x8e"), ":negative_squared_cross_mark:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9e\x95"), ":heavy_plus_sign:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9e\x96"), ":heavy_minus_sign:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9e\x97"), ":heavy_division_sign:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9e\xb0"), ":curly_loop:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9e\xbf"), ":loop:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe3\x80\xbd"), ":part_alternation_mark:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9c\xb3"), ":eight_spoked_asterisk:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9c\xb4"), ":eight_pointed_black_star:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9d\x87"), ":sparkle:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x80\xbc"), ":bangbang:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x81\x89"), ":interrobang:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x9d\x93"), ":question:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9d\x94"), ":grey_question:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9d\x95"), ":grey_exclamation:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9d\x97"), ":exclamation:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe3\x80\xb0"), ":wavy_dash:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xc2\xa9"), ":copyright:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xc2\xae"), ":registered:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x84\xa2"), ":tm:"}), QVariant::fromValue(Emoji{QString::fromUtf8("#\xe2\x83\xa3"), ":hash:"}), QVariant::fromValue( Emoji{QString::fromUtf8("*\xe2\x83\xa3"), ":asterisk:"}), QVariant::fromValue(Emoji{QString::fromUtf8("0\xe2\x83\xa3"), ":zero:"}), QVariant::fromValue(Emoji{QString::fromUtf8("1\xe2\x83\xa3"), ":one:"}), QVariant::fromValue(Emoji{QString::fromUtf8("2\xe2\x83\xa3"), ":two:"}), QVariant::fromValue(Emoji{QString::fromUtf8("3\xe2\x83\xa3"), ":three:"}), QVariant::fromValue(Emoji{QString::fromUtf8("4\xe2\x83\xa3"), ":four:"}), QVariant::fromValue(Emoji{QString::fromUtf8("5\xe2\x83\xa3"), ":five:"}), QVariant::fromValue(Emoji{QString::fromUtf8("6\xe2\x83\xa3"), ":six:"}), QVariant::fromValue(Emoji{QString::fromUtf8("7\xe2\x83\xa3"), ":seven:"}), QVariant::fromValue(Emoji{QString::fromUtf8("8\xe2\x83\xa3"), ":eight:"}), QVariant::fromValue(Emoji{QString::fromUtf8("9\xe2\x83\xa3"), ":nine:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x9f"), ":keycap_ten:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\xaf"), ":100:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xa0"), ":capital_abcd:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\xa1"), ":abcd:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\xa2"), ":1234:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xa3"), ":symbols:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\xa4"), ":abc:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x85\xb0"), ":a:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x86\x8e"), ":ab:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x85\xb1"), ":b:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x86\x91"), ":cl:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x86\x92"), ":cool:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x86\x93"), ":free:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x84\xb9"), ":information_source:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x86\x94"), ":id:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x93\x82"), ":m:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x86\x95"), ":new:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x86\x96"), ":ng:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x85\xbe"), ":o2:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x86\x97"), ":ok:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x85\xbf"), ":parking:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x86\x98"), ":sos:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x86\x99"), ":up:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x86\x9a"), ":vs:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x88\x81"), ":koko:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x88\x82"), ":sa:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x88\xb7"), ":u6708:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x88\xb6"), ":u6709:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x88\xaf"), ":u6307:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x89\x90"), ":ideograph_advantage:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x88\xb9"), ":u5272:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x88\x9a"), ":u7121:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x88\xb2"), ":u7981:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x89\x91"), ":accept:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x88\xb8"), ":u7533:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x88\xb4"), ":u5408:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x88\xb3"), ":u7a7a:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe3\x8a\x97"), ":congratulations:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe3\x8a\x99"), ":secret:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x88\xba"), ":u55b6:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x88\xb5"), ":u6e80:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x96\xaa"), ":black_small_square:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x96\xab"), ":white_small_square:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x97\xbb"), ":white_medium_square:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x97\xbc"), ":black_medium_square:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x97\xbd"), ":white_medium_small_square:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xe2\x97\xbe"), ":black_medium_small_square:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\xac\x9b"), ":black_large_square:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\xac\x9c"), ":white_large_square:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xb6"), ":large_orange_diamond:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xb7"), ":large_blue_diamond:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xb8"), ":small_orange_diamond:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xb9"), ":small_blue_diamond:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xba"), ":small_red_triangle:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x94\xbb"), ":small_red_triangle_down:"}), QVariant::fromValue(Emoji{QString::fromUtf8("\xf0\x9f\x92\xa0"), ":diamond_shape_with_a_dot_inside:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\x98"), ":radio_button:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xb2"), ":black_square_button:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xb3"), ":white_square_button:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9a\xaa"), ":white_circle:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xe2\x9a\xab"), ":black_circle:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xb4"), ":red_circle:"}), QVariant::fromValue( Emoji{QString::fromUtf8("\xf0\x9f\x94\xb5"), ":blue_circle:"}), }; const QVariantList EmojiModel::flags = { QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xa8"), ":flag_ac:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xa9"), ":flag_ad:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xaa"), ":flag_ae:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xab"), ":flag_af:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xac"), ":flag_ag:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xae"), ":flag_ai:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb1"), ":flag_al:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb2"), ":flag_am:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb4"), ":flag_ao:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb6"), ":flag_aq:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb7"), ":flag_ar:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb8"), ":flag_as:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xb9"), ":flag_at:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xba"), ":flag_au:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbc"), ":flag_aw:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbd"), ":flag_ax:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa6\xf0\x9f\x87\xbf"), ":flag_az:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa6"), ":flag_ba:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa7"), ":flag_bb:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xa9"), ":flag_bd:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xaa"), ":flag_be:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xab"), ":flag_bf:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xac"), ":flag_bg:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xad"), ":flag_bh:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xae"), ":flag_bi:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xaf"), ":flag_bj:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb1"), ":flag_bl:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb2"), ":flag_bm:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb3"), ":flag_bn:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb4"), ":flag_bo:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb6"), ":flag_bq:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb7"), ":flag_br:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb8"), ":flag_bs:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xb9"), ":flag_bt:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbb"), ":flag_bv:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbc"), ":flag_bw:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbe"), ":flag_by:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa7\xf0\x9f\x87\xbf"), ":flag_bz:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa6"), ":flag_ca:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa8"), ":flag_cc:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xa9"), ":flag_cd:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xab"), ":flag_cf:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xac"), ":flag_cg:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xad"), ":flag_ch:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xae"), ":flag_ci:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb0"), ":flag_ck:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb1"), ":flag_cl:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb2"), ":flag_cm:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb3"), ":flag_cn:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb4"), ":flag_co:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb5"), ":flag_cp:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xb7"), ":flag_cr:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xba"), ":flag_cu:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbb"), ":flag_cv:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbc"), ":flag_cw:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbd"), ":flag_cx:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbe"), ":flag_cy:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa8\xf0\x9f\x87\xbf"), ":flag_cz:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xaa"), ":flag_de:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xac"), ":flag_dg:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xaf"), ":flag_dj:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb0"), ":flag_dk:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb2"), ":flag_dm:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xb4"), ":flag_do:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xa9\xf0\x9f\x87\xbf"), ":flag_dz:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xa6"), ":flag_ea:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xa8"), ":flag_ec:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xaa"), ":flag_ee:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xac"), ":flag_eg:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xad"), ":flag_eh:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb7"), ":flag_er:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb8"), ":flag_es:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xb9"), ":flag_et:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xaa\xf0\x9f\x87\xba"), ":flag_eu:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xae"), ":flag_fi:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xaf"), ":flag_fj:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb0"), ":flag_fk:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb2"), ":flag_fm:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb4"), ":flag_fo:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xab\xf0\x9f\x87\xb7"), ":flag_fr:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa6"), ":flag_ga:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa7"), ":flag_gb:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xa9"), ":flag_gd:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xaa"), ":flag_ge:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xab"), ":flag_gf:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xac"), ":flag_gg:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xad"), ":flag_gh:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xae"), ":flag_gi:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb1"), ":flag_gl:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb2"), ":flag_gm:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb3"), ":flag_gn:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb5"), ":flag_gp:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb6"), ":flag_gq:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb7"), ":flag_gr:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb8"), ":flag_gs:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xb9"), ":flag_gt:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xba"), ":flag_gu:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xbc"), ":flag_gw:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xac\xf0\x9f\x87\xbe"), ":flag_gy:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb0"), ":flag_hk:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb2"), ":flag_hm:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb3"), ":flag_hn:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb7"), ":flag_hr:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xb9"), ":flag_ht:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xad\xf0\x9f\x87\xba"), ":flag_hu:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xa8"), ":flag_ic:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xa9"), ":flag_id:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xaa"), ":flag_ie:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb1"), ":flag_il:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb2"), ":flag_im:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb3"), ":flag_in:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb4"), ":flag_io:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb6"), ":flag_iq:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb7"), ":flag_ir:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb8"), ":flag_is:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xae\xf0\x9f\x87\xb9"), ":flag_it:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xaa"), ":flag_je:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb2"), ":flag_jm:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb4"), ":flag_jo:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xaf\xf0\x9f\x87\xb5"), ":flag_jp:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xaa"), ":flag_ke:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xac"), ":flag_kg:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xad"), ":flag_kh:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xae"), ":flag_ki:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb2"), ":flag_km:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb3"), ":flag_kn:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb5"), ":flag_kp:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xb7"), ":flag_kr:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbc"), ":flag_kw:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbe"), ":flag_ky:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb0\xf0\x9f\x87\xbf"), ":flag_kz:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa6"), ":flag_la:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa7"), ":flag_lb:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xa8"), ":flag_lc:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xae"), ":flag_li:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb0"), ":flag_lk:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb7"), ":flag_lr:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb8"), ":flag_ls:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xb9"), ":flag_lt:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xba"), ":flag_lu:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xbb"), ":flag_lv:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb1\xf0\x9f\x87\xbe"), ":flag_ly:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa6"), ":flag_ma:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa8"), ":flag_mc:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xa9"), ":flag_md:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xaa"), ":flag_me:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xab"), ":flag_mf:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xac"), ":flag_mg:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xad"), ":flag_mh:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb0"), ":flag_mk:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb1"), ":flag_ml:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb2"), ":flag_mm:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb3"), ":flag_mn:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb4"), ":flag_mo:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb5"), ":flag_mp:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb6"), ":flag_mq:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb7"), ":flag_mr:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb8"), ":flag_ms:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xb9"), ":flag_mt:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xba"), ":flag_mu:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbb"), ":flag_mv:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbc"), ":flag_mw:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbd"), ":flag_mx:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbe"), ":flag_my:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb2\xf0\x9f\x87\xbf"), ":flag_mz:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xa6"), ":flag_na:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xa8"), ":flag_nc:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xaa"), ":flag_ne:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xab"), ":flag_nf:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xac"), ":flag_ng:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xae"), ":flag_ni:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb1"), ":flag_nl:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb4"), ":flag_no:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb5"), ":flag_np:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xb7"), ":flag_nr:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xba"), ":flag_nu:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb3\xf0\x9f\x87\xbf"), ":flag_nz:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb4\xf0\x9f\x87\xb2"), ":flag_om:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xa6"), ":flag_pa:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xaa"), ":flag_pe:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xab"), ":flag_pf:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xac"), ":flag_pg:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xad"), ":flag_ph:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb0"), ":flag_pk:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb1"), ":flag_pl:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb2"), ":flag_pm:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb3"), ":flag_pn:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb7"), ":flag_pr:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb8"), ":flag_ps:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xb9"), ":flag_pt:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xbc"), ":flag_pw:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb5\xf0\x9f\x87\xbe"), ":flag_py:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb6\xf0\x9f\x87\xa6"), ":flag_qa:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xaa"), ":flag_re:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xb4"), ":flag_ro:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xb8"), ":flag_rs:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xba"), ":flag_ru:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb7\xf0\x9f\x87\xbc"), ":flag_rw:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa6"), ":flag_sa:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa7"), ":flag_sb:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa8"), ":flag_sc:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xa9"), ":flag_sd:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xaa"), ":flag_se:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xac"), ":flag_sg:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xad"), ":flag_sh:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xae"), ":flag_si:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xaf"), ":flag_sj:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb0"), ":flag_sk:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb1"), ":flag_sl:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb2"), ":flag_sm:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb3"), ":flag_sn:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb4"), ":flag_so:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb7"), ":flag_sr:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb8"), ":flag_ss:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xb9"), ":flag_st:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbb"), ":flag_sv:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbd"), ":flag_sx:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbe"), ":flag_sy:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb8\xf0\x9f\x87\xbf"), ":flag_sz:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa6"), ":flag_ta:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa8"), ":flag_tc:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xa9"), ":flag_td:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xab"), ":flag_tf:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xac"), ":flag_tg:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xad"), ":flag_th:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xaf"), ":flag_tj:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb0"), ":flag_tk:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb1"), ":flag_tl:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb2"), ":flag_tm:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb3"), ":flag_tn:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb4"), ":flag_to:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb7"), ":flag_tr:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xb9"), ":flag_tt:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbb"), ":flag_tv:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbc"), ":flag_tw:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xb9\xf0\x9f\x87\xbf"), ":flag_tz:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xa6"), ":flag_ua:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xac"), ":flag_ug:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xb2"), ":flag_um:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xb8"), ":flag_us:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xbe"), ":flag_uy:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xba\xf0\x9f\x87\xbf"), ":flag_uz:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xa6"), ":flag_va:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xa8"), ":flag_vc:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xaa"), ":flag_ve:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xac"), ":flag_vg:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xae"), ":flag_vi:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xb3"), ":flag_vn:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbb\xf0\x9f\x87\xba"), ":flag_vu:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbc\xf0\x9f\x87\xab"), ":flag_wf:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbc\xf0\x9f\x87\xb8"), ":flag_ws:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbd\xf0\x9f\x87\xb0"), ":flag_xk:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbe\xf0\x9f\x87\xaa"), ":flag_ye:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbe\xf0\x9f\x87\xb9"), ":flag_yt:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xa6"), ":flag_za:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xb2"), ":flag_zm:"}), QVariant::fromValue(Emoji{ QString::fromUtf8("\xf0\x9f\x87\xbf\xf0\x9f\x87\xbc"), ":flag_zw:"}), }; spectral/src/trayicon.h0000644000175000000620000000360213566674120015153 0ustar dilingerstaff#ifndef TRAYICON_H #define TRAYICON_H // Modified from mujx/nheko's TrayIcon. #include #include #include #include #include #include class MsgCountComposedIcon : public QIconEngine { public: MsgCountComposedIcon(const QString& filename); virtual void paint(QPainter* p, const QRect& rect, QIcon::Mode mode, QIcon::State state); virtual QIconEngine* clone() const; virtual QList availableSizes(QIcon::Mode mode, QIcon::State state) const; virtual QPixmap pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state); int msgCount = 0; bool isOnline = true; // Default to false? private: const int BubbleDiameter = 14; QIcon icon_; }; class TrayIcon : public QSystemTrayIcon { Q_OBJECT Q_PROPERTY(QString iconSource READ iconSource WRITE setIconSource NOTIFY iconSourceChanged) Q_PROPERTY(int notificationCount READ notificationCount WRITE setNotificationCount NOTIFY notificationCountChanged) Q_PROPERTY(bool isOnline READ isOnline WRITE setIsOnline NOTIFY isOnlineChanged) public: TrayIcon(QObject* parent = nullptr); QString iconSource() { return m_iconSource; } void setIconSource(const QString& source); int notificationCount() { return m_notificationCount; } void setNotificationCount(int count); bool isOnline() { return m_isOnline; } void setIsOnline(bool online); signals: void notificationCountChanged(); void iconSourceChanged(); void isOnlineChanged(); void showWindow(); private: QString m_iconSource; int m_notificationCount = 0; bool m_isOnline = true; QAction* viewAction_; QAction* quitAction_; MsgCountComposedIcon* icon_ = nullptr; }; #endif // TRAYICON_H spectral/src/trayicon.cpp0000644000175000000620000001125613566674120015512 0ustar dilingerstaff#include "trayicon.h" // Modified from mujx/nheko's TrayIcon. #include #include #include #include #include #if defined(Q_OS_MAC) #include #endif MsgCountComposedIcon::MsgCountComposedIcon(const QString& filename) : QIconEngine() { icon_ = QIcon(filename); } void MsgCountComposedIcon::paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) { painter->setRenderHint(QPainter::TextAntialiasing); painter->setRenderHint(QPainter::SmoothPixmapTransform); painter->setRenderHint(QPainter::Antialiasing); icon_.paint(painter, rect, Qt::AlignCenter, mode, state); if (isOnline && msgCount <= 0) return; QColor backgroundColor("red"); QColor textColor("white"); QBrush brush; brush.setStyle(Qt::SolidPattern); brush.setColor(backgroundColor); painter->setBrush(brush); painter->setPen(Qt::NoPen); painter->setFont(QFont("Open Sans", 8, QFont::Black)); QRectF bubble(rect.width() - BubbleDiameter, rect.height() - BubbleDiameter, BubbleDiameter, BubbleDiameter); painter->drawEllipse(bubble); painter->setPen(QPen(textColor)); painter->setBrush(Qt::NoBrush); if (!isOnline) { painter->drawText(bubble, Qt::AlignCenter, "x"); } else if (msgCount >= 100) { painter->drawText(bubble, Qt::AlignCenter, "99+"); } else { painter->drawText(bubble, Qt::AlignCenter, QString::number(msgCount)); } } QIconEngine* MsgCountComposedIcon::clone() const { return new MsgCountComposedIcon(*this); } QList MsgCountComposedIcon::availableSizes(QIcon::Mode mode, QIcon::State state) const { Q_UNUSED(mode) Q_UNUSED(state) QList sizes; sizes.append(QSize(24, 24)); sizes.append(QSize(32, 32)); sizes.append(QSize(48, 48)); sizes.append(QSize(64, 64)); sizes.append(QSize(128, 128)); sizes.append(QSize(256, 256)); return sizes; } QPixmap MsgCountComposedIcon::pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state) { QImage img(size, QImage::Format_ARGB32); img.fill(qRgba(0, 0, 0, 0)); QPixmap result = QPixmap::fromImage(img, Qt::NoFormatConversion); { QPainter painter(&result); paint(&painter, QRect(QPoint(0, 0), size), mode, state); } return result; } TrayIcon::TrayIcon(QObject* parent) : QSystemTrayIcon(parent) { QMenu* menu = new QMenu(); viewAction_ = new QAction(tr("Show"), parent); quitAction_ = new QAction(tr("Quit"), parent); connect(viewAction_, &QAction::triggered, this, &TrayIcon::showWindow); connect(quitAction_, &QAction::triggered, this, QApplication::quit); menu->addAction(viewAction_); menu->addAction(quitAction_); setContextMenu(menu); } void TrayIcon::setNotificationCount(int count) { m_notificationCount = count; // Use the native badge counter in MacOS. #if defined(Q_OS_MAC) auto labelText = count == 0 ? "" : QString::number(count); if (labelText == QtMac::badgeLabelText()) return; QtMac::setBadgeLabelText(labelText); #elif defined(Q_OS_WIN) // FIXME: Find a way to use Windows apis for the badge counter (if any). #else if (!icon_ || count == icon_->msgCount) return; // Custom drawing on Linux. MsgCountComposedIcon* tmp = static_cast(icon_->clone()); tmp->msgCount = count; setIcon(QIcon(tmp)); icon_ = tmp; #endif emit notificationCountChanged(); } void TrayIcon::setIsOnline(bool online) { m_isOnline = online; #if defined(Q_OS_MAC) if (online) { auto labelText = m_notificationCount == 0 ? "" : QString::number(m_notificationCount); if (labelText == QtMac::badgeLabelText()) return; QtMac::setBadgeLabelText(labelText); } else { auto labelText = "x"; if (labelText == QtMac::badgeLabelText()) return; QtMac::setBadgeLabelText(labelText); } #elif defined(Q_OS_WIN) // FIXME: Find a way to use Windows apis for the badge counter (if any). #else if (!icon_ || online == icon_->isOnline) return; // Custom drawing on Linux. MsgCountComposedIcon* tmp = static_cast(icon_->clone()); tmp->isOnline = online; setIcon(QIcon(tmp)); icon_ = tmp; #endif emit isOnlineChanged(); } void TrayIcon::setIconSource(const QString& source) { m_iconSource = source; #if defined(Q_OS_MAC) || defined(Q_OS_WIN) setIcon(QIcon(source)); #else icon_ = new MsgCountComposedIcon(source); setIcon(QIcon(icon_)); icon_->isOnline = m_isOnline; icon_->msgCount = m_notificationCount; #endif emit iconSourceChanged(); } spectral/src/userlistmodel.h0000644000175000000620000000244113566674120016216 0ustar dilingerstaff#ifndef USERLISTMODEL_H #define USERLISTMODEL_H #include "room.h" #include #include namespace Quotient { class Connection; class Room; class User; } // namespace Quotient class UserListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY( Quotient::Room* room READ room WRITE setRoom NOTIFY roomChanged) public: enum EventRoles { NameRole = Qt::UserRole + 1, UserIDRole, AvatarRole, ObjectRole }; using User = Quotient::User; UserListModel(QObject* parent = nullptr); Quotient::Room* room() const { return m_currentRoom; } void setRoom(Quotient::Room* room); User* userAt(QModelIndex index) const; QVariant data(const QModelIndex& index, int role = NameRole) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; QHash roleNames() const override; signals: void roomChanged(); private slots: void userAdded(User* user); void userRemoved(User* user); void refresh(User* user, QVector roles = {}); void avatarChanged(User* user, const Quotient::Room* context); private: Quotient::Room* m_currentRoom; QList m_users; int findUserPos(User* user) const; int findUserPos(const QString& username) const; }; #endif // USERLISTMODEL_H spectral/src/spectralroom.cpp0000644000175000000620000004566413566674120016406 0ustar dilingerstaff#include "spectralroom.h" #include "connection.h" #include "user.h" #include "csapi/account-data.h" #include "csapi/content-repo.h" #include "csapi/leaving.h" #include "csapi/room_state.h" #include "csapi/rooms.h" #include "csapi/typing.h" #include "events/accountdataevents.h" #include "events/reactionevent.h" #include "events/roommessageevent.h" #include "events/typingevent.h" #include "jobs/downloadfilejob.h" #include #include #include #include #include #include #include #include #include "utils.h" SpectralRoom::SpectralRoom(Connection* connection, QString roomId, JoinState joinState) : Room(connection, std::move(roomId), joinState) { connect(this, &SpectralRoom::notificationCountChanged, this, &SpectralRoom::countChanged); connect(this, &SpectralRoom::highlightCountChanged, this, &SpectralRoom::countChanged); connect(this, &Room::fileTransferCompleted, this, [=] { setFileUploadingProgress(0); setHasFileUploading(false); }); } void SpectralRoom::uploadFile(const QUrl& url, const QString& body) { if (url.isEmpty()) return; QString txnId = postFile(body.isEmpty() ? url.fileName() : body, url, false); setHasFileUploading(true); connect(this, &Room::fileTransferCompleted, [=](QString id, QUrl /*localFile*/, QUrl /*mxcUrl*/) { if (id == txnId) { setFileUploadingProgress(0); setHasFileUploading(false); } }); connect(this, &Room::fileTransferFailed, [=](QString id, QString /*error*/) { if (id == txnId) { setFileUploadingProgress(0); setHasFileUploading(false); } }); connect( this, &Room::fileTransferProgress, [=](QString id, qint64 progress, qint64 total) { if (id == txnId) { qDebug() << "Progress:" << progress << total; setFileUploadingProgress(int(float(progress) / float(total) * 100)); } }); } void SpectralRoom::acceptInvitation() { connection()->joinRoom(id()); } void SpectralRoom::forget() { connection()->forgetRoom(id()); } QVariantList SpectralRoom::getUsersTyping() const { auto users = usersTyping(); users.removeAll(localUser()); QVariantList userVariants; for (User* user : users) { userVariants.append(QVariant::fromValue(user)); } return userVariants; } void SpectralRoom::sendTypingNotification(bool isTyping) { connection()->callApi(BackgroundRequest, localUser()->id(), id(), isTyping, 10000); } QString SpectralRoom::lastEvent() const { for (auto i = messageEvents().rbegin(); i < messageEvents().rend(); i++) { const RoomEvent* evt = i->get(); if (is(*evt) || is(*evt)) continue; if (evt->isRedacted()) continue; if (evt->isStateEvent() && static_cast(*evt).repeatsState()) continue; if (auto e = eventCast(evt)) { if (!e->replacedEvent().isEmpty()) { continue; } } if (connection()->isIgnored(user(evt->senderId()))) continue; return user(evt->senderId())->displayname() + (evt->isStateEvent() ? " " : ": ") + eventToString(*evt); } return ""; } bool SpectralRoom::isEventHighlighted(const RoomEvent* e) const { return highlights.contains(e); } void SpectralRoom::checkForHighlights(const QMatrixClient::TimelineItem& ti) { auto localUserId = localUser()->id(); if (ti->senderId() == localUserId) return; if (auto* e = ti.viewAs()) { const auto& text = e->plainBody(); if (text.contains(localUserId) || text.contains(roomMembername(localUserId))) highlights.insert(e); } } void SpectralRoom::onAddNewTimelineEvents(timeline_iter_t from) { std::for_each(from, messageEvents().cend(), [this](const TimelineItem& ti) { checkForHighlights(ti); }); } void SpectralRoom::onAddHistoricalTimelineEvents(rev_iter_t from) { std::for_each(from, messageEvents().crend(), [this](const TimelineItem& ti) { checkForHighlights(ti); }); } void SpectralRoom::onRedaction(const RoomEvent& prevEvent, const RoomEvent& /*after*/) { if (const auto& e = eventCast(&prevEvent)) { if (auto relatedEventId = e->relation().eventId; !relatedEventId.isEmpty()) { emit updatedEvent(relatedEventId); } } } void SpectralRoom::countChanged() { if (displayed() && !hasUnreadMessages()) { resetNotificationCount(); resetHighlightCount(); } } QDateTime SpectralRoom::lastActiveTime() const { if (timelineSize() == 0) return QDateTime(); return messageEvents().rbegin()->get()->timestamp(); } int SpectralRoom::savedTopVisibleIndex() const { return firstDisplayedMarker() == timelineEdge() ? 0 : int(firstDisplayedMarker() - messageEvents().rbegin()); } int SpectralRoom::savedBottomVisibleIndex() const { return lastDisplayedMarker() == timelineEdge() ? 0 : int(lastDisplayedMarker() - messageEvents().rbegin()); } void SpectralRoom::saveViewport(int topIndex, int bottomIndex) { if (topIndex == -1 || bottomIndex == -1 || (bottomIndex == savedBottomVisibleIndex() && (bottomIndex == 0 || topIndex == savedTopVisibleIndex()))) return; if (bottomIndex == 0) { setFirstDisplayedEventId({}); setLastDisplayedEventId({}); return; } setFirstDisplayedEvent(maxTimelineIndex() - topIndex); setLastDisplayedEvent(maxTimelineIndex() - bottomIndex); } QVariantList SpectralRoom::getUsers(const QString& keyword) const { const auto userList = users(); QVariantList matchedList; for (const auto u : userList) if (u->displayname(this).contains(keyword, Qt::CaseInsensitive)) { matchedList.append(QVariant::fromValue(u)); } return matchedList; } QUrl SpectralRoom::urlToMxcUrl(QUrl mxcUrl) { return DownloadFileJob::makeRequestUrl(connection()->homeserver(), mxcUrl); } QString SpectralRoom::avatarMediaId() const { if (const auto avatar = Room::avatarMediaId(); !avatar.isEmpty()) { return avatar; } // Use the first (excluding self) user's avatar for direct chats const auto dcUsers = directChatUsers(); for (const auto u : dcUsers) { if (u != localUser()) { return u->avatarMediaId(); } } return {}; } QString SpectralRoom::eventToString(const RoomEvent& evt, Qt::TextFormat format, bool removeReply) const { const bool prettyPrint = (format == Qt::RichText); using namespace QMatrixClient; return visit( evt, [prettyPrint, removeReply](const RoomMessageEvent& e) { using namespace MessageEventContent; if (prettyPrint && e.hasTextContent() && e.mimeType().name() != "text/plain") { auto htmlBody = static_cast(e.content())->body; if (removeReply) { htmlBody.remove(utils::removeRichReplyRegex); } htmlBody.replace(utils::userPillRegExp, "\\1"); htmlBody.replace(utils::strikethroughRegExp, "\\1"); htmlBody.push_front(""); return htmlBody; } if (e.hasFileContent()) { auto fileCaption = e.content()->fileInfo()->originalName.toHtmlEscaped(); if (fileCaption.isEmpty()) { fileCaption = prettyPrint ? QMatrixClient::prettyPrint(e.plainBody()) : e.plainBody(); } else if (e.content()->fileInfo()->originalName != e.plainBody()) { fileCaption = e.plainBody() + " | " + fileCaption; } return !fileCaption.isEmpty() ? fileCaption : tr("a file"); } if (prettyPrint) { auto plainBody = e.plainBody(); if (removeReply) { plainBody.remove(utils::removeReplyRegex); } return QMatrixClient::prettyPrint(plainBody); } if (removeReply) { return e.plainBody().remove(utils::removeReplyRegex); } return e.plainBody(); }, [this](const RoomMemberEvent& e) { // FIXME: Rewind to the name that was at the time of this event auto subjectName = this->user(e.userId())->displayname(); // The below code assumes senderName output in AuthorRole switch (e.membership()) { case MembershipType::Invite: if (e.repeatsState()) return tr("reinvited %1 to the room").arg(subjectName); case MembershipType::Join: { if (e.repeatsState()) return tr("joined the room (repeated)"); if (!e.prevContent() || e.membership() != e.prevContent()->membership) { return e.membership() == MembershipType::Invite ? tr("invited %1 to the room").arg(subjectName) : tr("joined the room"); } QString text{}; if (e.isRename()) { if (e.displayName().isEmpty()) text = tr("cleared their display name"); else text = tr("changed their display name to %1") .arg(e.displayName().toHtmlEscaped()); } if (e.isAvatarUpdate()) { if (!text.isEmpty()) text += " and "; if (e.avatarUrl().isEmpty()) text += tr("cleared their avatar"); else if (e.prevContent()->avatarUrl.isEmpty()) text += tr("set an avatar"); else text += tr("updated their avatar"); } return text; } case MembershipType::Leave: if (e.prevContent() && e.prevContent()->membership == MembershipType::Invite) { return (e.senderId() != e.userId()) ? tr("withdrew %1's invitation").arg(subjectName) : tr("rejected the invitation"); } if (e.prevContent() && e.prevContent()->membership == MembershipType::Ban) { return (e.senderId() != e.userId()) ? tr("unbanned %1").arg(subjectName) : tr("self-unbanned"); } return (e.senderId() != e.userId()) ? tr("has put %1 out of the room: %2") .arg(subjectName, e.contentJson()["reason"_ls] .toString() .toHtmlEscaped()) : tr("left the room"); case MembershipType::Ban: return (e.senderId() != e.userId()) ? tr("banned %1 from the room: %2") .arg(subjectName, e.contentJson()["reason"_ls] .toString() .toHtmlEscaped()) : tr("self-banned from the room"); case MembershipType::Knock: return tr("knocked"); default:; } return tr("made something unknown"); }, [](const RoomAliasesEvent& e) { return tr("has set room aliases on server %1 to: %2") .arg(e.stateKey(), QLocale().createSeparatedList(e.aliases())); }, [](const RoomCanonicalAliasEvent& e) { return (e.alias().isEmpty()) ? tr("cleared the room main alias") : tr("set the room main alias to: %1").arg(e.alias()); }, [](const RoomNameEvent& e) { return (e.name().isEmpty()) ? tr("cleared the room name") : tr("set the room name to: %1") .arg(e.name().toHtmlEscaped()); }, [prettyPrint](const RoomTopicEvent& e) { return (e.topic().isEmpty()) ? tr("cleared the topic") : tr("set the topic to: %1") .arg(prettyPrint ? QMatrixClient::prettyPrint(e.topic()) : e.topic()); }, [](const RoomAvatarEvent&) { return tr("changed the room avatar"); }, [](const EncryptionEvent&) { return tr("activated End-to-End Encryption"); }, [](const RoomCreateEvent& e) { return (e.isUpgrade() ? tr("upgraded the room to version %1") : tr("created the room, version %1")) .arg(e.version().isEmpty() ? "1" : e.version().toHtmlEscaped()); }, [](const StateEventBase& e) { // A small hack for state events from TWIM bot return e.stateKey() == "twim" ? tr("updated the database", "TWIM bot updated the database") : e.stateKey().isEmpty() ? tr("updated %1 state", "%1 - Matrix event type") .arg(e.matrixType()) : tr("updated %1 state for %2", "%1 - Matrix event type, %2 - state key") .arg(e.matrixType(), e.stateKey().toHtmlEscaped()); }, tr("Unknown event")); } void SpectralRoom::changeAvatar(QUrl localFile) { const auto job = connection()->uploadFile(localFile.toLocalFile()); if (isJobRunning(job)) { connect(job, &BaseJob::success, this, [this, job] { connection()->callApi( id(), "m.room.avatar", QJsonObject{{"url", job->contentUri()}}); }); } } void SpectralRoom::addLocalAlias(const QString& alias) { auto aliases = localAliases(); if (aliases.contains(alias)) return; aliases += alias; setLocalAliases(aliases); } void SpectralRoom::removeLocalAlias(const QString& alias) { auto aliases = localAliases(); if (!aliases.contains(alias)) return; aliases.removeAll(alias); setLocalAliases(aliases); } QString SpectralRoom::markdownToHTML(const QString& markdown) { const auto str = markdown.toUtf8(); char* tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_DEFAULT); const std::string html(tmp_buf); free(tmp_buf); auto result = QString::fromStdString(html).trimmed(); result.replace("

", ""); result.replace("

", ""); return result; } void SpectralRoom::postArbitaryMessage(const QString& text, MessageEventType type, const QString& replyEventId) { const auto parsedHTML = markdownToHTML(text); const bool isRichText = Qt::mightBeRichText(parsedHTML); if (isRichText) { // Markdown postHtmlMessage(text, parsedHTML, type, replyEventId); } else { // Plain text postPlainMessage(text, type, replyEventId); } } QString msgTypeToString(MessageEventType msgType) { switch (msgType) { case MessageEventType::Text: return "m.text"; case MessageEventType::File: return "m.file"; case MessageEventType::Audio: return "m.audio"; case MessageEventType::Emote: return "m.emote"; case MessageEventType::Image: return "m.image"; case MessageEventType::Video: return "m.video"; case MessageEventType::Notice: return "m.notice"; case MessageEventType::Location: return "m.location"; default: return "m.text"; } } void SpectralRoom::postPlainMessage(const QString& text, MessageEventType type, const QString& replyEventId) { bool isReply = !replyEventId.isEmpty(); const auto replyIt = findInTimeline(replyEventId); if (replyIt == timelineEdge()) isReply = false; if (isReply) { const auto& replyEvt = **replyIt; QJsonObject json{ {"msgtype", msgTypeToString(type)}, {"body", "> <" + replyEvt.senderId() + "> " + eventToString(replyEvt) + "\n\n" + text}, {"format", "org.matrix.custom.html"}, {"m.relates_to", QJsonObject{ {"m.in_reply_to", QJsonObject{{"event_id", replyEventId}}}}}, {"formatted_body", "
In reply to " + replyEvt.senderId() + "
" + eventToString(replyEvt, Qt::RichText) + "
" + text.toHtmlEscaped()}}; postJson("m.room.message", json); return; } Room::postMessage(text, type); } void SpectralRoom::postHtmlMessage(const QString& text, const QString& html, MessageEventType type, const QString& replyEventId) { bool isReply = !replyEventId.isEmpty(); const auto replyIt = findInTimeline(replyEventId); if (replyIt == timelineEdge()) isReply = false; if (isReply) { const auto& replyEvt = **replyIt; QJsonObject json{ {"msgtype", msgTypeToString(type)}, {"body", "> <" + replyEvt.senderId() + "> " + eventToString(replyEvt) + "\n\n" + text}, {"format", "org.matrix.custom.html"}, {"m.relates_to", QJsonObject{ {"m.in_reply_to", QJsonObject{{"event_id", replyEventId}}}}}, {"formatted_body", "
In reply to " + replyEvt.senderId() + "
" + eventToString(replyEvt, Qt::RichText) + "
" + html}}; postJson("m.room.message", json); return; } Room::postHtmlMessage(text, html, type); } void SpectralRoom::toggleReaction(const QString& eventId, const QString& reaction) { if (eventId.isEmpty() || reaction.isEmpty()) return; const auto eventIt = findInTimeline(eventId); if (eventIt == timelineEdge()) return; const auto& evt = **eventIt; QStringList redactEventIds; // What if there are multiple reaction events? const auto& annotations = relatedEvents(evt, EventRelation::Annotation()); if (!annotations.isEmpty()) { for (const auto& a : annotations) { if (auto e = eventCast(a)) { if (e->relation().key != reaction) continue; if (e->senderId() == localUser()->id()) { redactEventIds.push_back(e->id()); break; } } } } if (!redactEventIds.isEmpty()) { for (const auto& redactEventId : redactEventIds) { redactEvent(redactEventId); } } else { postReaction(eventId, reaction); } } spectral/src/accountlistmodel.cpp0000644000175000000620000000442513566674120017233 0ustar dilingerstaff#include "accountlistmodel.h" #include "room.h" AccountListModel::AccountListModel(QObject* parent) : QAbstractListModel(parent) {} void AccountListModel::setController(Controller* value) { if (m_controller == value) { return; } beginResetModel(); m_connections.clear(); m_controller = value; m_connections += m_controller->connections(); connect(m_controller, &Controller::connectionAdded, this, [=](Connection* conn) { if (!conn) { return; } beginInsertRows(QModelIndex(), m_connections.count(), m_connections.count()); m_connections.append(conn); endInsertRows(); }); connect(m_controller, &Controller::connectionDropped, this, [=](Connection* conn) { qDebug() << "Dropping connection" << conn->userId(); if (!conn) { qDebug() << "Trying to remove null connection"; return; } conn->disconnect(this); const auto it = std::find(m_connections.begin(), m_connections.end(), conn); if (it == m_connections.end()) return; // Already deleted, nothing to do const int row = it - m_connections.begin(); beginRemoveRows(QModelIndex(), row, row); m_connections.erase(it); endRemoveRows(); }); emit controllerChanged(); } QVariant AccountListModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return {}; } if (index.row() >= m_connections.count()) { qDebug() << "AccountListModel, something's wrong: index.row() >= " "m_users.count()"; return {}; } auto m_connection = m_connections.at(index.row()); if (role == UserRole) { return QVariant::fromValue(m_connection->user()); } if (role == ConnectionRole) { return QVariant::fromValue(m_connection); } return {}; } int AccountListModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; } return m_connections.count(); } QHash AccountListModel::roleNames() const { QHash roles; roles[UserRole] = "user"; roles[ConnectionRole] = "connection"; return roles; } spectral/src/userlistmodel.cpp0000644000175000000620000000772213566674120016560 0ustar dilingerstaff#include "userlistmodel.h" #include #include #include #include #include #include #include "spectraluser.h" UserListModel::UserListModel(QObject* parent) : QAbstractListModel(parent), m_currentRoom(nullptr) {} void UserListModel::setRoom(QMatrixClient::Room* room) { if (m_currentRoom == room) return; using namespace QMatrixClient; beginResetModel(); if (m_currentRoom) { m_currentRoom->disconnect(this); // m_currentRoom->connection()->disconnect(this); for (User* user : m_users) user->disconnect(this); m_users.clear(); } m_currentRoom = room; if (m_currentRoom) { connect(m_currentRoom, &Room::userAdded, this, &UserListModel::userAdded); connect(m_currentRoom, &Room::userRemoved, this, &UserListModel::userRemoved); connect(m_currentRoom, &Room::memberAboutToRename, this, &UserListModel::userRemoved); connect(m_currentRoom, &Room::memberRenamed, this, &UserListModel::userAdded); { m_users = m_currentRoom->users(); std::sort(m_users.begin(), m_users.end(), room->memberSorter()); } for (User* user : m_users) { connect(user, &User::avatarChanged, this, &UserListModel::avatarChanged); } connect(m_currentRoom->connection(), &Connection::loggedOut, this, [=] { setRoom(nullptr); }); qDebug() << m_users.count() << "user(s) in the room"; } endResetModel(); emit roomChanged(); } QMatrixClient::User* UserListModel::userAt(QModelIndex index) const { if (index.row() < 0 || index.row() >= m_users.size()) return nullptr; return m_users.at(index.row()); } QVariant UserListModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() >= m_users.count()) { qDebug() << "UserListModel, something's wrong: index.row() >= m_users.count()"; return QVariant(); } auto user = m_users.at(index.row()); if (role == NameRole) { return user->displayname(m_currentRoom); } if (role == UserIDRole) { return user->id(); } if (role == AvatarRole) { return user->avatarMediaId(m_currentRoom); } if (role == ObjectRole) { return QVariant::fromValue(user); } return QVariant(); } int UserListModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_users.count(); } void UserListModel::userAdded(QMatrixClient::User* user) { auto pos = findUserPos(user); beginInsertRows(QModelIndex(), pos, pos); m_users.insert(pos, user); endInsertRows(); connect(user, &QMatrixClient::User::avatarChanged, this, &UserListModel::avatarChanged); } void UserListModel::userRemoved(QMatrixClient::User* user) { auto pos = findUserPos(user); if (pos != m_users.size()) { beginRemoveRows(QModelIndex(), pos, pos); m_users.removeAt(pos); endRemoveRows(); user->disconnect(this); } else qWarning() << "Trying to remove a room member not in the user list"; } void UserListModel::refresh(QMatrixClient::User* user, QVector roles) { auto pos = findUserPos(user); if (pos != m_users.size()) emit dataChanged(index(pos), index(pos), roles); else qWarning() << "Trying to access a room member not in the user list"; } void UserListModel::avatarChanged(QMatrixClient::User* user, const QMatrixClient::Room* context) { if (context == m_currentRoom) refresh(user, {AvatarRole}); } int UserListModel::findUserPos(User* user) const { return findUserPos(m_currentRoom->roomMembername(user)); } int UserListModel::findUserPos(const QString& username) const { return m_currentRoom->memberSorter().lowerBoundIndex(m_users, username); } QHash UserListModel::roleNames() const { QHash roles; roles[NameRole] = "name"; roles[UserIDRole] = "userId"; roles[AvatarRole] = "avatar"; roles[ObjectRole] = "user"; return roles; } spectral/src/controller.h0000644000175000000620000000706313566674120015513 0ustar dilingerstaff#ifndef CONTROLLER_H #define CONTROLLER_H #include "connection.h" #include "notifications/manager.h" #include "room.h" #include "settings.h" #include "user.h" #include #include #include #include #include #include using namespace QMatrixClient; class Controller : public QObject { Q_OBJECT Q_PROPERTY(int accountCount READ accountCount NOTIFY connectionAdded NOTIFY connectionDropped) Q_PROPERTY(bool quitOnLastWindowClosed READ quitOnLastWindowClosed WRITE setQuitOnLastWindowClosed NOTIFY quitOnLastWindowClosedChanged) Q_PROPERTY(Connection* connection READ connection WRITE setConnection NOTIFY connectionChanged) Q_PROPERTY(bool isOnline READ isOnline NOTIFY isOnlineChanged) Q_PROPERTY(bool busy READ busy WRITE setBusy NOTIFY busyChanged) public: explicit Controller(QObject* parent = nullptr); ~Controller(); Q_INVOKABLE void loginWithCredentials(QString, QString, QString, QString); Q_INVOKABLE void loginWithAccessToken(QString, QString, QString, QString); QVector connections() const { return m_connections; } // All the non-Q_INVOKABLE functions. void addConnection(Connection* c); void dropConnection(Connection* c); // All the Q_PROPERTYs. int accountCount() { return m_connections.count(); } bool quitOnLastWindowClosed() const { return QApplication::quitOnLastWindowClosed(); } void setQuitOnLastWindowClosed(bool value) { if (quitOnLastWindowClosed() != value) { QApplication::setQuitOnLastWindowClosed(value); emit quitOnLastWindowClosedChanged(); } } bool isOnline() const { return m_ncm.isOnline(); } bool busy() const { return m_busy; } void setBusy(bool busy) { if (m_busy == busy) { return; } m_busy = busy; emit busyChanged(); } Connection* connection() const { if (m_connection.isNull()) return nullptr; return m_connection; } void setConnection(Connection* conn) { if (conn == m_connection) return; m_connection = conn; emit connectionChanged(); } private: QVector m_connections; QPointer m_connection; QNetworkConfigurationManager m_ncm; bool m_busy = false; QByteArray loadAccessTokenFromFile(const AccountSettings& account); QByteArray loadAccessTokenFromKeyChain(const AccountSettings& account); bool saveAccessTokenToFile(const AccountSettings& account, const QByteArray& accessToken); bool saveAccessTokenToKeyChain(const AccountSettings& account, const QByteArray& accessToken); void loadSettings(); void saveSettings() const; private slots: void invokeLogin(); signals: void busyChanged(); void errorOccured(QString error, QString detail); void syncDone(); void connectionAdded(Connection* conn); void connectionDropped(Connection* conn); void initiated(); void notificationClicked(const QString roomId, const QString eventId); void quitOnLastWindowClosedChanged(); void unreadCountChanged(); void connectionChanged(); void isOnlineChanged(); public slots: void logout(Connection* conn); void joinRoom(Connection* c, const QString& alias); void createRoom(Connection* c, const QString& name, const QString& topic); void createDirectChat(Connection* c, const QString& userID); void playAudio(QUrl localFile); void changeAvatar(Connection* conn, QUrl localFile); void markAllMessagesAsRead(Connection* conn); }; #endif // CONTROLLER_H spectral/src/roomlistmodel.h0000644000175000000620000000431413566674120016215 0ustar dilingerstaff#ifndef ROOMLISTMODEL_H #define ROOMLISTMODEL_H #include "connection.h" #include "events/roomevent.h" #include "room.h" #include "spectralroom.h" #include using namespace Quotient; class RoomType : public QObject { Q_OBJECT public: enum Types { Invited = 1, Favorite, Direct, Normal, Deprioritized, }; Q_ENUMS(Types) }; class RoomListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(Connection* connection READ connection WRITE setConnection) Q_PROPERTY(int notificationCount READ notificationCount NOTIFY notificationCountChanged) public: enum EventRoles { NameRole = Qt::UserRole + 1, AvatarRole, TopicRole, CategoryRole, UnreadCountRole, NotificationCountRole, HighlightCountRole, LastEventRole, LastActiveTimeRole, JoinStateRole, CurrentRoomRole, }; RoomListModel(QObject* parent = nullptr); virtual ~RoomListModel() override; Connection* connection() const { return m_connection; } void setConnection(Connection* connection); void doResetModel(); Q_INVOKABLE SpectralRoom* roomAt(int row) const; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; Q_INVOKABLE int rowCount( const QModelIndex& parent = QModelIndex()) const override; QHash roleNames() const override; int notificationCount() const { return m_notificationCount; } private slots: void doAddRoom(Room* room); void updateRoom(Room* room, Room* prev); void deleteRoom(Room* room); void refresh(SpectralRoom* room, const QVector& roles = {}); void refreshNotificationCount(); private: Connection* m_connection = nullptr; QList m_rooms; int m_notificationCount = 0; void connectRoomSignals(SpectralRoom* room); signals: void connectionChanged(); void notificationCountChanged(); void roomAdded(SpectralRoom* room); void newMessage(const QString& roomId, const QString& eventId, const QString& roomName, const QString& senderName, const QString& text, const QImage& icon); }; #endif // ROOMLISTMODEL_H spectral/src/matriximageprovider.h0000644000175000000620000000330613566674120017406 0ustar dilingerstaff#ifndef MatrixImageProvider_H #define MatrixImageProvider_H #pragma once #include #include #include #include #include namespace Quotient { class Connection; } class ThumbnailResponse : public QQuickImageResponse { Q_OBJECT public: ThumbnailResponse(Quotient::Connection* c, QString mediaId, const QSize& requestedSize); ~ThumbnailResponse() override = default; private slots: void startRequest(); void prepareResult(); void doCancel(); private: Quotient::Connection* c; const QString mediaId; const QSize requestedSize; const QString localFile; Quotient::MediaThumbnailJob* job = nullptr; QImage image; QString errorStr; mutable QReadWriteLock lock; // Guards ONLY these two members above QQuickTextureFactory* textureFactory() const override; QString errorString() const override; void cancel() override; }; class MatrixImageProvider : public QObject, public QQuickAsyncImageProvider { Q_OBJECT Q_PROPERTY(Quotient::Connection* connection READ connection WRITE setConnection NOTIFY connectionChanged) public: explicit MatrixImageProvider() = default; QQuickImageResponse* requestImageResponse( const QString& id, const QSize& requestedSize) override; Quotient::Connection* connection() { return m_connection; } void setConnection(Quotient::Connection* connection) { m_connection.store(connection); emit connectionChanged(); } signals: void connectionChanged(); private: QAtomicPointer m_connection; }; #endif // MatrixImageProvider_H spectral/src/matriximageprovider.cpp0000644000175000000620000000654613566674120017752 0ustar dilingerstaff#include "matriximageprovider.h" #include #include #include #include #include using QMatrixClient::BaseJob; ThumbnailResponse::ThumbnailResponse(QMatrixClient::Connection* c, QString id, const QSize& size) : c(c), mediaId(std::move(id)), requestedSize(size), localFile(QStringLiteral("%1/image_provider/%2-%3x%4.png") .arg(QStandardPaths::writableLocation( QStandardPaths::CacheLocation), mediaId, QString::number(requestedSize.width()), QString::number(requestedSize.height()))), errorStr("Image request hasn't started") { if (requestedSize.isEmpty()) { errorStr.clear(); emit finished(); return; } if (mediaId.count('/') != 1) { errorStr = tr("Media id '%1' doesn't follow server/mediaId pattern").arg(mediaId); emit finished(); return; } QImage cachedImage; if (cachedImage.load(localFile)) { image = cachedImage; errorStr.clear(); emit finished(); return; } // Execute a request on the main thread asynchronously moveToThread(c->thread()); QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, Qt::QueuedConnection); } void ThumbnailResponse::startRequest() { // Runs in the main thread, not QML thread Q_ASSERT(QThread::currentThread() == c->thread()); job = c->getThumbnail(mediaId, requestedSize); // Connect to any possible outcome including abandonment // to make sure the QML thread is not left stuck forever. connect(job, &BaseJob::finished, this, &ThumbnailResponse::prepareResult); } void ThumbnailResponse::prepareResult() { Q_ASSERT(QThread::currentThread() == job->thread()); Q_ASSERT(job->error() != BaseJob::Pending); { QWriteLocker _(&lock); if (job->error() == BaseJob::Success) { image = job->thumbnail(); QString localPath = QFileInfo(localFile).absolutePath(); QDir dir; if (!dir.exists(localPath)) dir.mkpath(localPath); image.save(localFile); errorStr.clear(); } else if (job->error() == BaseJob::Abandoned) { errorStr = tr("Image request has been cancelled"); qDebug() << "ThumbnailResponse: cancelled for" << mediaId; } else { errorStr = job->errorString(); qWarning() << "ThumbnailResponse: no valid image for" << mediaId << "-" << errorStr; } job = nullptr; } emit finished(); } void ThumbnailResponse::doCancel() { // Runs in the main thread, not QML thread if (job) { Q_ASSERT(QThread::currentThread() == job->thread()); job->abandon(); } } QQuickTextureFactory* ThumbnailResponse::textureFactory() const { QReadLocker _(&lock); return QQuickTextureFactory::textureFactoryForImage(image); } QString ThumbnailResponse::errorString() const { QReadLocker _(&lock); return errorStr; } void ThumbnailResponse::cancel() { QMetaObject::invokeMethod(this, &ThumbnailResponse::doCancel, Qt::QueuedConnection); } QQuickImageResponse* MatrixImageProvider::requestImageResponse( const QString& id, const QSize& requestedSize) { return new ThumbnailResponse(m_connection.load(), id, requestedSize); } spectral/src/messageeventmodel.cpp0000644000175000000620000004627013566674120017375 0ustar dilingerstaff#include "messageeventmodel.h" #include #include #include #include #include #include #include #include #include #include // for qmlRegisterType() #include "utils.h" QHash MessageEventModel::roleNames() const { QHash roles = QAbstractItemModel::roleNames(); roles[EventTypeRole] = "eventType"; roles[MessageRole] = "message"; roles[EventIdRole] = "eventId"; roles[TimeRole] = "time"; roles[SectionRole] = "section"; roles[AuthorRole] = "author"; roles[ContentRole] = "content"; roles[ContentTypeRole] = "contentType"; roles[HighlightRole] = "highlight"; roles[ReadMarkerRole] = "readMarker"; roles[SpecialMarksRole] = "marks"; roles[LongOperationRole] = "progressInfo"; roles[AnnotationRole] = "annotation"; roles[EventResolvedTypeRole] = "eventResolvedType"; roles[ReplyRole] = "reply"; roles[UserMarkerRole] = "userMarker"; roles[ShowAuthorRole] = "showAuthor"; roles[ShowSectionRole] = "showSection"; roles[BubbleShapeRole] = "bubbleShape"; roles[ReactionRole] = "reaction"; return roles; } MessageEventModel::MessageEventModel(QObject* parent) : QAbstractListModel(parent), m_currentRoom(nullptr) { using namespace QMatrixClient; qmlRegisterType(); qRegisterMetaType(); qmlRegisterUncreatableType( "Spectral", 0, 1, "EventStatus", "EventStatus is not an creatable type"); } MessageEventModel::~MessageEventModel() {} void MessageEventModel::setRoom(SpectralRoom* room) { if (room == m_currentRoom) return; beginResetModel(); if (m_currentRoom) { m_currentRoom->disconnect(this); } m_currentRoom = room; if (room) { lastReadEventId = room->readMarkerEventId(); using namespace QMatrixClient; connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [=](RoomEventsRange events) { beginInsertRows({}, timelineBaseIndex(), timelineBaseIndex() + int(events.size()) - 1); }); connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [=](RoomEventsRange events) { if (rowCount() > 0) rowBelowInserted = rowCount() - 1; // See #312 beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1); }); connect(m_currentRoom, &Room::addedMessages, this, [=](int lowest, int biggest) { endInsertRows(); if (biggest < m_currentRoom->maxTimelineIndex()) { auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1; refreshEventRoles(rowBelowInserted, {ShowAuthorRole, BubbleShapeRole}); } for (auto i = m_currentRoom->maxTimelineIndex() - biggest; i <= m_currentRoom->maxTimelineIndex() - lowest; ++i) refreshLastUserEvents(i); }); connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this] { beginInsertRows({}, 0, 0); }); connect(m_currentRoom, &Room::pendingEventAdded, this, [=] { endInsertRows(); refreshEventRoles(1, {ShowAuthorRole, BubbleShapeRole}); }); connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, [this](RoomEvent*, int i) { if (i == 0) return; // No need to move anything, just refresh movingEvent = true; // Reverse i because row 0 is bottommost in the model const auto row = timelineBaseIndex() - i - 1; Q_ASSERT(beginMoveRows({}, row, row, {}, timelineBaseIndex())); }); connect(m_currentRoom, &Room::pendingEventMerged, this, [this] { if (movingEvent) { endMoveRows(); movingEvent = false; } refreshRow(timelineBaseIndex()); // Refresh the looks refreshLastUserEvents(0); if (m_currentRoom->timelineSize() > 1) // Refresh above refreshEventRoles(timelineBaseIndex() + 1, {ReadMarkerRole}); if (timelineBaseIndex() > 0) // Refresh below, see #312 refreshEventRoles(timelineBaseIndex() - 1, {ShowAuthorRole, BubbleShapeRole}); }); connect(m_currentRoom, &Room::pendingEventChanged, this, &MessageEventModel::refreshRow); connect(m_currentRoom, &Room::pendingEventAboutToDiscard, this, [this](int i) { beginRemoveRows({}, i, i); }); connect(m_currentRoom, &Room::pendingEventDiscarded, this, &MessageEventModel::endRemoveRows); connect(m_currentRoom, &Room::readMarkerMoved, this, [this] { refreshEventRoles( std::exchange(lastReadEventId, m_currentRoom->readMarkerEventId()), {ReadMarkerRole}); refreshEventRoles(lastReadEventId, {ReadMarkerRole}); }); connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent* newEvent) { refreshLastUserEvents(refreshEvent(newEvent->id()) - timelineBaseIndex()); }); connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString& eventId) { if (eventId.isEmpty()) { // How did we get here? return; } refreshEventRoles(eventId, {ReactionRole, Qt::DisplayRole}); }); connect(m_currentRoom, &Room::fileTransferProgress, this, &MessageEventModel::refreshEvent); connect(m_currentRoom, &Room::fileTransferCompleted, this, &MessageEventModel::refreshEvent); connect(m_currentRoom, &Room::fileTransferFailed, this, &MessageEventModel::refreshEvent); connect(m_currentRoom, &Room::fileTransferCancelled, this, &MessageEventModel::refreshEvent); connect(m_currentRoom, &Room::readMarkerForUserMoved, this, [=](User*, QString fromEventId, QString toEventId) { refreshEventRoles(fromEventId, {UserMarkerRole}); refreshEventRoles(toEventId, {UserMarkerRole}); }); connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [=] { beginResetModel(); endResetModel(); }); qDebug() << "Connected to room" << room->id() << "as" << room->localUser()->id(); } else lastReadEventId.clear(); endResetModel(); } int MessageEventModel::refreshEvent(const QString& eventId) { return refreshEventRoles(eventId); } void MessageEventModel::refreshRow(int row) { refreshEventRoles(row); } int MessageEventModel::timelineBaseIndex() const { return m_currentRoom ? int(m_currentRoom->pendingEvents().size()) : 0; } void MessageEventModel::refreshEventRoles(int row, const QVector& roles) { const auto idx = index(row); emit dataChanged(idx, idx, roles); } int MessageEventModel::refreshEventRoles(const QString& id, const QVector& roles) { // On 64-bit platforms, difference_type for std containers is long long // but Qt uses int throughout its interfaces; hence casting to int below. int row = -1; // First try pendingEvents because it is almost always very short. const auto pendingIt = m_currentRoom->findPendingEvent(id); if (pendingIt != m_currentRoom->pendingEvents().end()) row = int(pendingIt - m_currentRoom->pendingEvents().begin()); else { const auto timelineIt = m_currentRoom->findInTimeline(id); if (timelineIt == m_currentRoom->timelineEdge()) { qWarning() << "Trying to refresh inexistent event:" << id; return -1; } row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex(); } refreshEventRoles(row, roles); return row; } inline bool hasValidTimestamp(const QMatrixClient::TimelineItem& ti) { return ti->timestamp().isValid(); } QDateTime MessageEventModel::makeMessageTimestamp( const QMatrixClient::Room::rev_iter_t& baseIt) const { const auto& timeline = m_currentRoom->messageEvents(); auto ts = baseIt->event()->timestamp(); if (ts.isValid()) return ts; // The event is most likely redacted or just invalid. // Look for the nearest date around and slap zero time to it. using QMatrixClient::TimelineItem; auto rit = std::find_if(baseIt, timeline.rend(), hasValidTimestamp); if (rit != timeline.rend()) return {rit->event()->timestamp().date(), {0, 0}, Qt::LocalTime}; auto it = std::find_if(baseIt.base(), timeline.end(), hasValidTimestamp); if (it != timeline.end()) return {it->event()->timestamp().date(), {0, 0}, Qt::LocalTime}; // What kind of room is that?.. qCritical() << "No valid timestamps in the room timeline!"; return {}; } QString MessageEventModel::renderDate(QDateTime timestamp) const { auto date = timestamp.toLocalTime().date(); if (date == QDate::currentDate()) return tr("Today"); if (date == QDate::currentDate().addDays(-1)) return tr("Yesterday"); if (date == QDate::currentDate().addDays(-2)) return tr("The day before yesterday"); if (date > QDate::currentDate().addDays(-7)) return date.toString("dddd"); return date.toString(Qt::DefaultLocaleShortDate); } void MessageEventModel::refreshLastUserEvents(int baseTimelineRow) { if (!m_currentRoom || m_currentRoom->timelineSize() <= baseTimelineRow) return; const auto& timelineBottom = m_currentRoom->messageEvents().rbegin(); const auto& lastSender = (*(timelineBottom + baseTimelineRow))->senderId(); const auto limit = timelineBottom + std::min(baseTimelineRow + 10, m_currentRoom->timelineSize()); for (auto it = timelineBottom + std::max(baseTimelineRow - 10, 0); it != limit; ++it) { if ((*it)->senderId() == lastSender) { auto idx = index(it - timelineBottom); emit dataChanged(idx, idx); } } } int MessageEventModel::rowCount(const QModelIndex& parent) const { if (!m_currentRoom || parent.isValid()) return 0; return m_currentRoom->timelineSize(); } inline QVariantMap userAtEvent(SpectralUser* user, SpectralRoom* room, const RoomEvent& evt) { return QVariantMap{ {"isLocalUser", user->id() == room->localUser()->id()}, {"id", user->id()}, {"avatarMediaId", user->avatarMediaId(room)}, {"avatarUrl", user->avatarUrl(room)}, {"displayName", user->displayname(room)}, {"color", user->color()}, {"object", QVariant::fromValue(user)}, }; } QVariant MessageEventModel::data(const QModelIndex& idx, int role) const { const auto row = idx.row(); if (!m_currentRoom || row < 0 || row >= int(m_currentRoom->pendingEvents().size()) + m_currentRoom->timelineSize()) return {}; bool isPending = row < timelineBaseIndex(); const auto timelineIt = m_currentRoom->messageEvents().crbegin() + std::max(0, row - timelineBaseIndex()); const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex()); const auto& evt = isPending ? **pendingIt : **timelineIt; if (role == Qt::DisplayRole) { return m_currentRoom->eventToString(evt, Qt::RichText); } if (role == MessageRole) { return m_currentRoom->eventToString(evt); } if (role == Qt::ToolTipRole) { return evt.originalJson(); } if (role == EventTypeRole) { if (auto e = eventCast(&evt)) { switch (e->msgtype()) { case MessageEventType::Emote: return "emote"; case MessageEventType::Notice: return "notice"; case MessageEventType::Image: return "image"; case MessageEventType::Audio: return "audio"; case MessageEventType::Video: return "video"; default: break; } if (e->hasFileContent()) { return "file"; } return "message"; } if (evt.isStateEvent()) return "state"; return "other"; } if (role == EventResolvedTypeRole) return EventTypeRegistry::getMatrixType(evt.type()); if (role == AuthorRole) { auto author = static_cast(isPending ? m_currentRoom->localUser() : m_currentRoom->user(evt.senderId())); return userAtEvent(author, m_currentRoom, evt); } if (role == ContentTypeRole) { if (auto e = eventCast(&evt)) { const auto& contentType = e->mimeType().name(); return contentType == "text/plain" ? QStringLiteral("text/html") : contentType; } return QStringLiteral("text/plain"); } if (role == ContentRole) { if (evt.isRedacted()) { auto reason = evt.redactedBecause()->reason(); return (reason.isEmpty()) ? tr("[REDACTED]") : tr("[REDACTED: %1]").arg(evt.redactedBecause()->reason()); } if (auto e = eventCast(&evt)) { // Cannot use e.contentJson() here because some // EventContent classes inject values into the copy of the // content JSON stored in EventContent::Base return e->hasFileContent() ? QVariant::fromValue(e->content()->originalJson) : QVariant(); }; } if (role == HighlightRole) return m_currentRoom->isEventHighlighted(&evt); if (role == ReadMarkerRole) return evt.id() == lastReadEventId && row > timelineBaseIndex(); if (role == SpecialMarksRole) { if (isPending) return pendingIt->deliveryStatus(); auto* memberEvent = timelineIt->viewAs(); if (memberEvent) { if ((memberEvent->isJoin() || memberEvent->isLeave()) && !Settings().value("UI/show_joinleave", true).toBool()) return EventStatus::Hidden; } if (is(evt) || is(evt)) return EventStatus::Hidden; if (evt.isRedacted()) return EventStatus::Hidden; if (evt.isStateEvent() && static_cast(evt).repeatsState()) return EventStatus::Hidden; if (auto e = eventCast(&evt)) { if (!e->replacedEvent().isEmpty()) { return EventStatus::Hidden; } } if (m_currentRoom->connection()->isIgnored( m_currentRoom->user(evt.senderId()))) return EventStatus::Hidden; return EventStatus::Normal; } if (role == EventIdRole) return !evt.id().isEmpty() ? evt.id() : evt.transactionId(); if (role == LongOperationRole) { if (auto e = eventCast(&evt)) if (e->hasFileContent()) return QVariant::fromValue(m_currentRoom->fileTransferInfo(e->id())); } if (role == AnnotationRole) if (isPending) return pendingIt->annotation(); if (role == TimeRole || role == SectionRole) { auto ts = isPending ? pendingIt->lastUpdated() : makeMessageTimestamp(timelineIt); return role == TimeRole ? QVariant(ts) : renderDate(ts); } if (role == UserMarkerRole) { QVariantList variantList; for (User* user : m_currentRoom->usersAtEventId(evt.id())) { if (user == m_currentRoom->localUser()) continue; variantList.append(QVariant::fromValue(user)); } return variantList; } if (role == ReplyRole) { const QString& replyEventId = evt.contentJson()["m.relates_to"] .toObject()["m.in_reply_to"] .toObject()["event_id"] .toString(); if (replyEventId.isEmpty()) return {}; const auto replyIt = m_currentRoom->findInTimeline(replyEventId); if (replyIt == m_currentRoom->timelineEdge()) return {}; const auto& replyEvt = **replyIt; return QVariantMap{ {"eventId", replyEventId}, {"display", m_currentRoom->eventToString(replyEvt, Qt::RichText)}, {"author", userAtEvent(static_cast(m_currentRoom->user(replyEvt.senderId())), m_currentRoom, evt)}}; } if (role == ShowAuthorRole) { for (auto r = row - 1; r >= 0; --r) { auto i = index(r); if (data(i, SpecialMarksRole) != EventStatus::Hidden) { return data(i, AuthorRole) != data(idx, AuthorRole) || data(i, EventTypeRole) != data(idx, EventTypeRole) || data(idx, TimeRole) .toDateTime() .msecsTo(data(i, TimeRole).toDateTime()) > 600000; } } return true; } if (role == ShowSectionRole) { for (auto r = row + 1; r < rowCount(); ++r) { auto i = index(r); if (data(i, SpecialMarksRole) != EventStatus::Hidden) { return data(i, TimeRole) .toDateTime() .msecsTo(data(idx, TimeRole).toDateTime()) > 600000; } } return true; } if (role == BubbleShapeRole) { // TODO: Convoluted logic. int aboveRow = -1; // Invalid for (auto r = row + 1; r < rowCount(); ++r) { auto i = index(r); if (data(i, SpecialMarksRole) != EventStatus::Hidden) { aboveRow = r; break; } } bool aboveShow, belowShow; if (aboveRow == -1) { aboveShow = true; } else { aboveShow = data(index(aboveRow), ShowAuthorRole).toBool(); } belowShow = data(idx, ShowAuthorRole).toBool(); if (aboveShow && belowShow) return BubbleShapes::NoShape; if (aboveShow && !belowShow) return BubbleShapes::BeginShape; if (belowShow) return BubbleShapes::EndShape; return BubbleShapes::MiddleShape; } if (role == ReactionRole) { const auto& annotations = m_currentRoom->relatedEvents(evt, EventRelation::Annotation()); if (annotations.isEmpty()) return {}; QMap> reactions = {}; for (const auto& a : annotations) { if (a->isRedacted()) // Just in case? continue; if (auto e = eventCast(a)) reactions[e->relation().key].append( static_cast(m_currentRoom->user(e->senderId()))); } if (reactions.isEmpty()) { return {}; } QVariantList res = {}; auto i = reactions.constBegin(); while (i != reactions.constEnd()) { QVariantList authors; for (auto author : i.value()) { authors.append(userAtEvent(author, m_currentRoom, evt)); } bool hasLocalUser = i.value().contains( static_cast(m_currentRoom->localUser())); res.append(QVariantMap{{"reaction", i.key()}, {"count", i.value().count()}, {"authors", authors}, {"hasLocalUser", hasLocalUser}}); ++i; } return res; } return {}; } int MessageEventModel::eventIDToIndex(const QString& eventID) const { const auto it = m_currentRoom->findInTimeline(eventID); if (it == m_currentRoom->timelineEdge()) { qWarning() << "Trying to find inexistent event:" << eventID; return -1; } return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex(); } spectral/src/accountlistmodel.h0000644000175000000620000000160513566674120016675 0ustar dilingerstaff#ifndef ACCOUNTLISTMODEL_H #define ACCOUNTLISTMODEL_H #include "controller.h" #include #include class AccountListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(Controller* controller READ controller WRITE setController NOTIFY controllerChanged) public: enum EventRoles { UserRole = Qt::UserRole + 1, ConnectionRole }; AccountListModel(QObject* parent = nullptr); QVariant data(const QModelIndex& index, int role = UserRole) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; QHash roleNames() const override; Controller* controller() const { return m_controller; } void setController(Controller* value); private: Controller* m_controller = nullptr; QVector m_connections; signals: void controllerChanged(); }; #endif // ACCOUNTLISTMODEL_H spectral/src/utils.h0000644000175000000620000000176713566674120014475 0ustar dilingerstaff#ifndef Utils_H #define Utils_H #include "room.h" #include "user.h" #include #include #include #include #include #include #include namespace utils { static const QRegularExpression removeReplyRegex{ "> <.*?>.*?\\n\\n", QRegularExpression::DotMatchesEverythingOption}; static const QRegularExpression removeRichReplyRegex{ ".*?", QRegularExpression::DotMatchesEverythingOption}; static const QRegularExpression codePillRegExp{ "
]*>(.*?)
", QRegularExpression::DotMatchesEverythingOption}; static const QRegularExpression userPillRegExp{ "(.*?)", QRegularExpression::DotMatchesEverythingOption}; static const QRegularExpression strikethroughRegExp{ "(.*?)", QRegularExpression::DotMatchesEverythingOption}; } // namespace utils #endif spectral/src/notifications/0002755000175000000620000000000013566674120016024 5ustar dilingerstaffspectral/src/notifications/managerwin.cpp0000644000175000000620000000466113566674120020665 0ustar dilingerstaff#include "manager.h" #include "wintoastlib.h" #include using namespace WinToastLib; class CustomHandler : public IWinToastHandler { public: CustomHandler(uint id, NotificationsManager *parent) : notificationID(id), notificationsManager(parent) {} void toastActivated() { notificationsManager->actionInvoked(notificationID, ""); } void toastActivated(int) { notificationsManager->actionInvoked(notificationID, ""); } void toastFailed() { std::wcout << L"Error showing current toast" << std::endl; } void toastDismissed(WinToastDismissalReason) { notificationsManager->notificationClosed(notificationID, 0); } private: uint notificationID; NotificationsManager *notificationsManager; }; namespace { bool isInitialized = false; uint count = 0; void init() { isInitialized = true; WinToast::instance()->setAppName(L"Spectral"); WinToast::instance()->setAppUserModelId( WinToast::configureAUMI(L"Spectral", L"Spectral")); if (!WinToast::instance()->initialize()) std::wcout << "Your system in not compatible with toast notifications\n"; } } // namespace NotificationsManager::NotificationsManager(QObject *parent) : QObject(parent) {} void NotificationsManager::postNotification( const QString &room_id, const QString &event_id, const QString &room_name, const QString &sender, const QString &text, const QImage &icon) { Q_UNUSED(room_id) Q_UNUSED(event_id) Q_UNUSED(icon) if (!isInitialized) init(); auto templ = WinToastTemplate(WinToastTemplate::ImageAndText02); if (room_name != sender) templ.setTextField( QString("%1 - %2").arg(sender).arg(room_name).toStdWString(), WinToastTemplate::FirstLine); else templ.setTextField(QString("%1").arg(sender).toStdWString(), WinToastTemplate::FirstLine); templ.setTextField(QString("%1").arg(text).toStdWString(), WinToastTemplate::SecondLine); count++; CustomHandler *customHandler = new CustomHandler(count, this); notificationIds[count] = roomEventId{room_id, event_id}; WinToast::instance()->showToast(templ, customHandler); } void NotificationsManager::actionInvoked(uint id, QString action) { if (notificationIds.contains(id)) { roomEventId idEntry = notificationIds[id]; emit notificationClicked(idEntry.roomId, idEntry.eventId); } } void NotificationsManager::notificationClosed(uint id, uint reason) { notificationIds.remove(id); } spectral/src/notifications/managermac.mm0000644000175000000620000000247113566674120020454 0ustar dilingerstaff#include "manager.h" #include #include #include @interface NSUserNotification (CFIPrivate) - (void)set_identityImage:(NSImage*)image; @end NotificationsManager::NotificationsManager(QObject* parent) : QObject(parent) {} void NotificationsManager::postNotification(const QString& roomId, const QString& eventId, const QString& roomName, const QString& senderName, const QString& text, const QImage& icon) { Q_UNUSED(roomId); Q_UNUSED(eventId); Q_UNUSED(icon); NSUserNotification* notif = [[NSUserNotification alloc] init]; notif.title = roomName.toNSString(); notif.subtitle = QString("%1 sent a message").arg(senderName).toNSString(); notif.informativeText = text.toNSString(); notif.soundName = NSUserNotificationDefaultSoundName; notif.contentImage = QtMac::toNSImage(QPixmap::fromImage(icon)); [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notif]; [notif autorelease]; } // unused void NotificationsManager::actionInvoked(uint, QString) {} void NotificationsManager::notificationClosed(uint, uint) {} spectral/src/notifications/wintoastlib.h0000644000175000000620000001643013566674120020536 0ustar dilingerstaff#ifndef WINTOASTLIB_H #define WINTOASTLIB_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Microsoft::WRL; using namespace ABI::Windows::Data::Xml::Dom; using namespace ABI::Windows::Foundation; using namespace ABI::Windows::UI::Notifications; using namespace Windows::Foundation; #define DEFAULT_SHELL_LINKS_PATH L"\\Microsoft\\Windows\\Start Menu\\Programs\\" #define DEFAULT_LINK_FORMAT L".lnk" namespace WinToastLib { class IWinToastHandler { public: enum WinToastDismissalReason { UserCanceled = ToastDismissalReason::ToastDismissalReason_UserCanceled, ApplicationHidden = ToastDismissalReason::ToastDismissalReason_ApplicationHidden, TimedOut = ToastDismissalReason::ToastDismissalReason_TimedOut }; virtual ~IWinToastHandler() {} virtual void toastActivated() = 0; virtual void toastActivated(int actionIndex) = 0; virtual void toastDismissed(WinToastDismissalReason state) = 0; virtual void toastFailed() = 0; }; class WinToastTemplate { public: enum Duration { System, Short, Long }; enum AudioOption { Default = 0, Silent = 1, Loop = 2 }; enum TextField { FirstLine = 0, SecondLine, ThirdLine }; enum WinToastTemplateType { ImageAndText01 = ToastTemplateType::ToastTemplateType_ToastImageAndText01, ImageAndText02 = ToastTemplateType::ToastTemplateType_ToastImageAndText02, ImageAndText03 = ToastTemplateType::ToastTemplateType_ToastImageAndText03, ImageAndText04 = ToastTemplateType::ToastTemplateType_ToastImageAndText04, Text01 = ToastTemplateType::ToastTemplateType_ToastText01, Text02 = ToastTemplateType::ToastTemplateType_ToastText02, Text03 = ToastTemplateType::ToastTemplateType_ToastText03, Text04 = ToastTemplateType::ToastTemplateType_ToastText04, WinToastTemplateTypeCount }; WinToastTemplate(_In_ WinToastTemplateType type = WinToastTemplateType::ImageAndText02); ~WinToastTemplate(); void setTextField(_In_ const std::wstring& txt, _In_ TextField pos); void setImagePath(_In_ const std::wstring& imgPath); void setAudioPath(_In_ const std::wstring& audioPath); void setAttributionText(_In_ const std::wstring & attributionText); void addAction(_In_ const std::wstring& label); void setAudioOption(_In_ WinToastTemplate::AudioOption audioOption); void setDuration(_In_ Duration duration); void setExpiration(_In_ INT64 millisecondsFromNow); std::size_t textFieldsCount() const; std::size_t actionsCount() const; bool hasImage() const; const std::vector& textFields() const; const std::wstring& textField(_In_ TextField pos) const; const std::wstring& actionLabel(_In_ int pos) const; const std::wstring& imagePath() const; const std::wstring& audioPath() const; const std::wstring& attributionText() const; INT64 expiration() const; WinToastTemplateType type() const; WinToastTemplate::AudioOption audioOption() const; Duration duration() const; private: std::vector _textFields; std::vector _actions; std::wstring _imagePath = L""; std::wstring _audioPath = L""; std::wstring _attributionText = L""; INT64 _expiration = 0; AudioOption _audioOption = WinToastTemplate::AudioOption::Default; WinToastTemplateType _type = WinToastTemplateType::Text01; Duration _duration = Duration::System; }; class WinToast { public: enum WinToastError { NoError = 0, NotInitialized, SystemNotSupported, ShellLinkNotCreated, InvalidAppUserModelID, InvalidParameters, InvalidHandler, NotDisplayed, UnknownError }; enum ShortcutResult { SHORTCUT_UNCHANGED = 0, SHORTCUT_WAS_CHANGED = 1, SHORTCUT_WAS_CREATED = 2, SHORTCUT_MISSING_PARAMETERS = -1, SHORTCUT_INCOMPATIBLE_OS = -2, SHORTCUT_COM_INIT_FAILURE = -3, SHORTCUT_CREATE_FAILED = -4 }; WinToast(void); virtual ~WinToast(); static WinToast* instance(); static bool isCompatible(); static bool isSupportingModernFeatures(); static std::wstring configureAUMI(_In_ const std::wstring& companyName, _In_ const std::wstring& productName, _In_ const std::wstring& subProduct = std::wstring(), _In_ const std::wstring& versionInformation = std::wstring() ); virtual bool initialize(_Out_ WinToastError* error = nullptr); virtual bool isInitialized() const; virtual bool hideToast(_In_ INT64 id); virtual INT64 showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHandler* handler, _Out_ WinToastError* error = nullptr); virtual void clear(); virtual enum ShortcutResult createShortcut(); const std::wstring& appName() const; const std::wstring& appUserModelId() const; void setAppUserModelId(_In_ const std::wstring& appName); void setAppName(_In_ const std::wstring& appName); protected: bool _isInitialized; bool _hasCoInitialized; std::wstring _appName; std::wstring _aumi; std::map> _buffer; HRESULT validateShellLinkHelper(_Out_ bool& wasChanged); HRESULT createShellLinkHelper(); HRESULT setImageFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path); HRESULT setAudioFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path, _In_opt_ WinToastTemplate::AudioOption option = WinToastTemplate::AudioOption::Default); HRESULT setTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text, _In_ int pos); HRESULT setAttributionTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text); HRESULT addActionHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& action, _In_ const std::wstring& arguments); HRESULT addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& duration); ComPtr notifier(_In_ bool* succeded) const; void setError(_Out_ WinToastError* error, _In_ WinToastError value); }; } #endif // WINTOASTLIB_H spectral/src/notifications/manager.h0000644000175000000620000000274213566674120017612 0ustar dilingerstaff#pragma once #include #include #include #include #include #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) #include #include #endif struct roomEventId { QString roomId; QString eventId; }; class NotificationsManager : public QObject { Q_OBJECT public: NotificationsManager(QObject* parent = nullptr); signals: void notificationClicked(const QString roomId, const QString eventId); private: #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) QDBusInterface dbus; uint showNotification(const QString summary, const QString text, const QImage image); #endif // notification ID to (room ID, event ID) QMap notificationIds; // these slots are platform specific (D-Bus only) // but Qt slot declarations can not be inside an ifdef! public slots: void actionInvoked(uint id, QString action); void notificationClosed(uint id, uint reason); void postNotification(const QString& roomId, const QString& eventId, const QString& roomName, const QString& senderName, const QString& text, const QImage& icon); }; #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) QDBusArgument& operator<<(QDBusArgument& arg, const QImage& image); const QDBusArgument& operator>>(const QDBusArgument& arg, QImage&); #endif spectral/src/notifications/managerlinux.cpp0000644000175000000620000001214013566674120021216 0ustar dilingerstaff#include "manager.h" #include #include #include #include #include NotificationsManager::NotificationsManager(QObject *parent) : QObject(parent), dbus("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", QDBusConnection::sessionBus(), this) { qDBusRegisterMetaType(); QDBusConnection::sessionBus().connect( "org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "ActionInvoked", this, SLOT(actionInvoked(uint, QString))); QDBusConnection::sessionBus().connect( "org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "NotificationClosed", this, SLOT(notificationClosed(uint, uint))); } void NotificationsManager::postNotification( const QString &roomid, const QString &eventid, const QString &roomname, const QString &sender, const QString &text, const QImage &icon) { uint id = showNotification(sender + " (" + roomname + ")", text, icon); notificationIds[id] = roomEventId{roomid, eventid}; } /** * This function is based on code from * https://github.com/rohieb/StratumsphereTrayIcon * Copyright (C) 2012 Roland Hieber * Licensed under the GNU General Public License, version 3 */ uint NotificationsManager::showNotification(const QString summary, const QString text, const QImage image) { QImage croppedImage; QRect rect = image.rect(); if (rect.width() != rect.height()) { if (rect.width() > rect.height()) { QRect crop((rect.width() - rect.height()) / 2, 0, rect.height(), rect.height()); croppedImage = image.copy(crop); } else { QRect crop(0, (rect.height() - rect.width()) / 2, rect.width(), rect.width()); croppedImage = image.copy(crop); } } else { croppedImage = image; } QVariantMap hints; hints["image-data"] = croppedImage; QList argumentList; argumentList << "Spectral"; // app_name argumentList << uint(0); // replace_id argumentList << ""; // app_icon argumentList << summary; // summary argumentList << text; // body argumentList << (QStringList("default") << "reply"); // actions argumentList << hints; // hints argumentList << int(-1); // timeout in ms static QDBusInterface notifyApp("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications"); QDBusMessage reply = notifyApp.callWithArgumentList(QDBus::AutoDetect, "Notify", argumentList); if (reply.type() == QDBusMessage::ErrorMessage) { qDebug() << "D-Bus Error:" << reply.errorMessage(); return 0; } else { return reply.arguments().first().toUInt(); } } void NotificationsManager::actionInvoked(uint id, QString action) { if (action == "default" && notificationIds.contains(id)) { roomEventId idEntry = notificationIds[id]; emit notificationClicked(idEntry.roomId, idEntry.eventId); } } void NotificationsManager::notificationClosed(uint id, uint reason) { Q_UNUSED(reason); notificationIds.remove(id); } /** * Automatic marshaling of a QImage for org.freedesktop.Notifications.Notify * * This function is from the Clementine project (see * http://www.clementine-player.org) and licensed under the GNU General Public * License, version 3 or later. * * Copyright 2010, David Sansome */ QDBusArgument &operator<<(QDBusArgument &arg, const QImage &image) { if (image.isNull()) { arg.beginStructure(); arg << 0 << 0 << 0 << false << 0 << 0 << QByteArray(); arg.endStructure(); return arg; } QImage scaled = image.scaledToHeight(100, Qt::SmoothTransformation); scaled = scaled.convertToFormat(QImage::Format_ARGB32); #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN // ABGR -> ARGB QImage i = scaled.rgbSwapped(); #else // ABGR -> GBAR QImage i(scaled.size(), scaled.format()); for (int y = 0; y < i.height(); ++y) { QRgb *p = (QRgb *)scaled.scanLine(y); QRgb *q = (QRgb *)i.scanLine(y); QRgb *end = p + scaled.width(); while (p < end) { *q = qRgba(qGreen(*p), qBlue(*p), qAlpha(*p), qRed(*p)); p++; q++; } } #endif arg.beginStructure(); arg << i.width(); arg << i.height(); arg << i.bytesPerLine(); arg << i.hasAlphaChannel(); int channels = i.isGrayscale() ? 1 : (i.hasAlphaChannel() ? 4 : 3); arg << i.depth() / channels; arg << channels; arg << QByteArray(reinterpret_cast(i.bits()), i.sizeInBytes()); arg.endStructure(); return arg; } const QDBusArgument &operator>>(const QDBusArgument &arg, QImage &) { // This is needed to link but shouldn't be called. Q_ASSERT(0); return arg; } spectral/src/notifications/wintoastlib.cpp0000644000175000000620000012166613566674120021101 0ustar dilingerstaff#include "wintoastlib.h" #include #include #pragma comment(lib,"shlwapi") #pragma comment(lib,"user32") #ifdef NDEBUG #define DEBUG_MSG(str) do { } while ( false ) #else #define DEBUG_MSG(str) do { std::wcout << str << std::endl; } while( false ) #endif // Thanks: https://stackoverflow.com/a/36545162/4297146 typedef LONG NTSTATUS, *PNTSTATUS; #define STATUS_SUCCESS (0x00000000) typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); RTL_OSVERSIONINFOW GetRealOSVersion() { HMODULE hMod = ::GetModuleHandleW(L"ntdll.dll"); if (hMod) { RtlGetVersionPtr fxPtr = (RtlGetVersionPtr)::GetProcAddress(hMod, "RtlGetVersion"); if (fxPtr != nullptr) { RTL_OSVERSIONINFOW rovi = { 0 }; rovi.dwOSVersionInfoSize = sizeof(rovi); if (STATUS_SUCCESS == fxPtr(&rovi)) { return rovi; } } } RTL_OSVERSIONINFOW rovi = { 0 }; return rovi; } // Quickstart: Handling toast activations from Win32 apps in Windows 10 // https://blogs.msdn.microsoft.com/tiles_and_toasts/2015/10/16/quickstart-handling-toast-activations-from-win32-apps-in-windows-10/ using namespace WinToastLib; namespace DllImporter { // Function load a function from library template HRESULT loadFunctionFromLibrary(HINSTANCE library, LPCSTR name, Function &func) { if (!library) { return E_INVALIDARG; } func = reinterpret_cast(GetProcAddress(library, name)); return (func != nullptr) ? S_OK : E_FAIL; } typedef HRESULT(FAR STDAPICALLTYPE *f_SetCurrentProcessExplicitAppUserModelID)(__in PCWSTR AppID); typedef HRESULT(FAR STDAPICALLTYPE *f_PropVariantToString)(_In_ REFPROPVARIANT propvar, _Out_writes_(cch) PWSTR psz, _In_ UINT cch); typedef HRESULT(FAR STDAPICALLTYPE *f_RoGetActivationFactory)(_In_ HSTRING activatableClassId, _In_ REFIID iid, _COM_Outptr_ void ** factory); typedef HRESULT(FAR STDAPICALLTYPE *f_WindowsCreateStringReference)(_In_reads_opt_(length + 1) PCWSTR sourceString, UINT32 length, _Out_ HSTRING_HEADER * hstringHeader, _Outptr_result_maybenull_ _Result_nullonfailure_ HSTRING * string); typedef PCWSTR(FAR STDAPICALLTYPE *f_WindowsGetStringRawBuffer)(_In_ HSTRING string, _Out_ UINT32 *length); typedef HRESULT(FAR STDAPICALLTYPE *f_WindowsDeleteString)(_In_opt_ HSTRING string); static f_SetCurrentProcessExplicitAppUserModelID SetCurrentProcessExplicitAppUserModelID; static f_PropVariantToString PropVariantToString; static f_RoGetActivationFactory RoGetActivationFactory; static f_WindowsCreateStringReference WindowsCreateStringReference; static f_WindowsGetStringRawBuffer WindowsGetStringRawBuffer; static f_WindowsDeleteString WindowsDeleteString; template _Check_return_ __inline HRESULT _1_GetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ T** factory) { return RoGetActivationFactory(activatableClassId, IID_INS_ARGS(factory)); } template inline HRESULT Wrap_GetActivationFactory(_In_ HSTRING activatableClassId, _Inout_ Details::ComPtrRef factory) throw() { return _1_GetActivationFactory(activatableClassId, factory.ReleaseAndGetAddressOf()); } inline HRESULT initialize() { HINSTANCE LibShell32 = LoadLibraryW(L"SHELL32.DLL"); HRESULT hr = loadFunctionFromLibrary(LibShell32, "SetCurrentProcessExplicitAppUserModelID", SetCurrentProcessExplicitAppUserModelID); if (SUCCEEDED(hr)) { HINSTANCE LibPropSys = LoadLibraryW(L"PROPSYS.DLL"); hr = loadFunctionFromLibrary(LibPropSys, "PropVariantToString", PropVariantToString); if (SUCCEEDED(hr)) { HINSTANCE LibComBase = LoadLibraryW(L"COMBASE.DLL"); const bool succeded = SUCCEEDED(loadFunctionFromLibrary(LibComBase, "RoGetActivationFactory", RoGetActivationFactory)) && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsCreateStringReference", WindowsCreateStringReference)) && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsGetStringRawBuffer", WindowsGetStringRawBuffer)) && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsDeleteString", WindowsDeleteString)); return succeded ? S_OK : E_FAIL; } } return hr; } } class WinToastStringWrapper { public: WinToastStringWrapper(_In_reads_(length) PCWSTR stringRef, _In_ UINT32 length) throw() { HRESULT hr = DllImporter::WindowsCreateStringReference(stringRef, length, &_header, &_hstring); if (!SUCCEEDED(hr)) { RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); } } WinToastStringWrapper(_In_ const std::wstring &stringRef) throw() { HRESULT hr = DllImporter::WindowsCreateStringReference(stringRef.c_str(), static_cast(stringRef.length()), &_header, &_hstring); if (FAILED(hr)) { RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); } } ~WinToastStringWrapper() { DllImporter::WindowsDeleteString(_hstring); } inline HSTRING Get() const throw() { return _hstring; } private: HSTRING _hstring; HSTRING_HEADER _header; }; class MyDateTime : public IReference { protected: DateTime _dateTime; public: static INT64 Now() { FILETIME now; GetSystemTimeAsFileTime(&now); return ((((INT64)now.dwHighDateTime) << 32) | now.dwLowDateTime); } MyDateTime(DateTime dateTime) : _dateTime(dateTime) {} MyDateTime(INT64 millisecondsFromNow) { _dateTime.UniversalTime = Now() + millisecondsFromNow * 10000; } operator INT64() { return _dateTime.UniversalTime; } HRESULT STDMETHODCALLTYPE get_Value(DateTime *dateTime) { *dateTime = _dateTime; return S_OK; } HRESULT STDMETHODCALLTYPE QueryInterface(const IID& riid, void** ppvObject) { if (!ppvObject) { return E_POINTER; } if (riid == __uuidof(IUnknown) || riid == __uuidof(IReference)) { *ppvObject = static_cast(static_cast*>(this)); return S_OK; } return E_NOINTERFACE; } ULONG STDMETHODCALLTYPE Release() { return 1; } ULONG STDMETHODCALLTYPE AddRef() { return 2; } HRESULT STDMETHODCALLTYPE GetIids(ULONG*, IID**) { return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE GetRuntimeClassName(HSTRING*) { return E_NOTIMPL; } HRESULT STDMETHODCALLTYPE GetTrustLevel(TrustLevel*) { return E_NOTIMPL; } }; namespace Util { inline HRESULT defaultExecutablePath(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { DWORD written = GetModuleFileNameExW(GetCurrentProcess(), nullptr, path, nSize); DEBUG_MSG("Default executable path: " << path); return (written > 0) ? S_OK : E_FAIL; } inline HRESULT defaultShellLinksDirectory(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { DWORD written = GetEnvironmentVariableW(L"APPDATA", path, nSize); HRESULT hr = written > 0 ? S_OK : E_INVALIDARG; if (SUCCEEDED(hr)) { errno_t result = wcscat_s(path, nSize, DEFAULT_SHELL_LINKS_PATH); hr = (result == 0) ? S_OK : E_INVALIDARG; DEBUG_MSG("Default shell link path: " << path); } return hr; } inline HRESULT defaultShellLinkPath(const std::wstring& appname, _In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { HRESULT hr = defaultShellLinksDirectory(path, nSize); if (SUCCEEDED(hr)) { const std::wstring appLink(appname + DEFAULT_LINK_FORMAT); errno_t result = wcscat_s(path, nSize, appLink.c_str()); hr = (result == 0) ? S_OK : E_INVALIDARG; DEBUG_MSG("Default shell link file path: " << path); } return hr; } inline PCWSTR AsString(ComPtr &xmlDocument) { HSTRING xml; ComPtr ser; HRESULT hr = xmlDocument.As(&ser); hr = ser->GetXml(&xml); if (SUCCEEDED(hr)) return DllImporter::WindowsGetStringRawBuffer(xml, NULL); return NULL; } inline PCWSTR AsString(HSTRING hstring) { return DllImporter::WindowsGetStringRawBuffer(hstring, NULL); } inline HRESULT setNodeStringValue(const std::wstring& string, IXmlNode *node, IXmlDocument *xml) { ComPtr textNode; HRESULT hr = xml->CreateTextNode( WinToastStringWrapper(string).Get(), &textNode); if (SUCCEEDED(hr)) { ComPtr stringNode; hr = textNode.As(&stringNode); if (SUCCEEDED(hr)) { ComPtr appendedChild; hr = node->AppendChild(stringNode.Get(), &appendedChild); } } return hr; } inline HRESULT setEventHandlers(_In_ IToastNotification* notification, _In_ std::shared_ptr eventHandler, _In_ INT64 expirationTime) { EventRegistrationToken activatedToken, dismissedToken, failedToken; HRESULT hr = notification->add_Activated( Callback < Implements < RuntimeClassFlags, ITypedEventHandler> >( [eventHandler](IToastNotification*, IInspectable* inspectable) { IToastActivatedEventArgs *activatedEventArgs; HRESULT hr = inspectable->QueryInterface(&activatedEventArgs); if (SUCCEEDED(hr)) { HSTRING argumentsHandle; hr = activatedEventArgs->get_Arguments(&argumentsHandle); if (SUCCEEDED(hr)) { PCWSTR arguments = Util::AsString(argumentsHandle); if (arguments && *arguments) { eventHandler->toastActivated((int)wcstol(arguments, NULL, 10)); return S_OK; } } } eventHandler->toastActivated(); return S_OK; }).Get(), &activatedToken); if (SUCCEEDED(hr)) { hr = notification->add_Dismissed(Callback < Implements < RuntimeClassFlags, ITypedEventHandler> >( [eventHandler, expirationTime](IToastNotification*, IToastDismissedEventArgs* e) { ToastDismissalReason reason; if (SUCCEEDED(e->get_Reason(&reason))) { if (reason == ToastDismissalReason_UserCanceled && expirationTime && MyDateTime::Now() >= expirationTime) reason = ToastDismissalReason_TimedOut; eventHandler->toastDismissed(static_cast(reason)); } return S_OK; }).Get(), &dismissedToken); if (SUCCEEDED(hr)) { hr = notification->add_Failed(Callback < Implements < RuntimeClassFlags, ITypedEventHandler> >( [eventHandler](IToastNotification*, IToastFailedEventArgs*) { eventHandler->toastFailed(); return S_OK; }).Get(), &failedToken); } } return hr; } inline HRESULT addAttribute(_In_ IXmlDocument *xml, const std::wstring &name, IXmlNamedNodeMap *attributeMap) { ComPtr srcAttribute; HRESULT hr = xml->CreateAttribute(WinToastStringWrapper(name).Get(), &srcAttribute); if (SUCCEEDED(hr)) { ComPtr node; hr = srcAttribute.As(&node); if (SUCCEEDED(hr)) { ComPtr pNode; hr = attributeMap->SetNamedItem(node.Get(), &pNode); } } return hr; } inline HRESULT createElement(_In_ IXmlDocument *xml, _In_ const std::wstring& root_node, _In_ const std::wstring& element_name, _In_ const std::vector& attribute_names) { ComPtr rootList; HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(root_node).Get(), &rootList); if (SUCCEEDED(hr)) { ComPtr root; hr = rootList->Item(0, &root); if (SUCCEEDED(hr)) { ComPtr audioElement; hr = xml->CreateElement(WinToastStringWrapper(element_name).Get(), &audioElement); if (SUCCEEDED(hr)) { ComPtr audioNodeTmp; hr = audioElement.As(&audioNodeTmp); if (SUCCEEDED(hr)) { ComPtr audioNode; hr = root->AppendChild(audioNodeTmp.Get(), &audioNode); if (SUCCEEDED(hr)) { ComPtr attributes; hr = audioNode->get_Attributes(&attributes); if (SUCCEEDED(hr)) { for (auto it : attribute_names) { hr = addAttribute(xml, it, attributes.Get()); } } } } } } } return hr; } } WinToast* WinToast::instance() { static WinToast instance; return &instance; } WinToast::WinToast() : _isInitialized(false), _hasCoInitialized(false) { if (!isCompatible()) { DEBUG_MSG(L"Warning: Your system is not compatible with this library "); } } WinToast::~WinToast() { if (_hasCoInitialized) { CoUninitialize(); } } void WinToast::setAppName(_In_ const std::wstring& appName) { _appName = appName; } void WinToast::setAppUserModelId(_In_ const std::wstring& aumi) { _aumi = aumi; DEBUG_MSG(L"Default App User Model Id: " << _aumi.c_str()); } bool WinToast::isCompatible() { DllImporter::initialize(); return !((DllImporter::SetCurrentProcessExplicitAppUserModelID == nullptr) || (DllImporter::PropVariantToString == nullptr) || (DllImporter::RoGetActivationFactory == nullptr) || (DllImporter::WindowsCreateStringReference == nullptr) || (DllImporter::WindowsDeleteString == nullptr)); } bool WinToastLib::WinToast::isSupportingModernFeatures() { RTL_OSVERSIONINFOW tmp = GetRealOSVersion(); return tmp.dwMajorVersion > 6; } std::wstring WinToast::configureAUMI(_In_ const std::wstring &companyName, _In_ const std::wstring &productName, _In_ const std::wstring &subProduct, _In_ const std::wstring &versionInformation) { std::wstring aumi = companyName; aumi += L"." + productName; if (subProduct.length() > 0) { aumi += L"." + subProduct; if (versionInformation.length() > 0) { aumi += L"." + versionInformation; } } if (aumi.length() > SCHAR_MAX) { DEBUG_MSG("Error: max size allowed for AUMI: 128 characters."); } return aumi; } enum WinToast::ShortcutResult WinToast::createShortcut() { if (_aumi.empty() || _appName.empty()) { DEBUG_MSG(L"Error: App User Model Id or Appname is empty!"); return SHORTCUT_MISSING_PARAMETERS; } if (!isCompatible()) { DEBUG_MSG(L"Your OS is not compatible with this library! =("); return SHORTCUT_INCOMPATIBLE_OS; } if (!_hasCoInitialized) { HRESULT initHr = CoInitializeEx(NULL, COINIT::COINIT_MULTITHREADED); if (initHr != RPC_E_CHANGED_MODE) { if (FAILED(initHr) && initHr != S_FALSE) { DEBUG_MSG(L"Error on COM library initialization!"); return SHORTCUT_COM_INIT_FAILURE; } else { _hasCoInitialized = true; } } } bool wasChanged; HRESULT hr = validateShellLinkHelper(wasChanged); if (SUCCEEDED(hr)) return wasChanged ? SHORTCUT_WAS_CHANGED : SHORTCUT_UNCHANGED; hr = createShellLinkHelper(); return SUCCEEDED(hr) ? SHORTCUT_WAS_CREATED : SHORTCUT_CREATE_FAILED; } bool WinToast::initialize(_Out_ WinToastError* error) { _isInitialized = false; setError(error, WinToastError::NoError); if (!isCompatible()) { setError(error, WinToastError::SystemNotSupported); DEBUG_MSG(L"Error: system not supported."); return false; } if (_aumi.empty() || _appName.empty()) { setError(error, WinToastError::InvalidParameters); DEBUG_MSG(L"Error while initializing, did you set up a valid AUMI and App name?"); return false; } if (createShortcut() < 0) { setError(error, WinToastError::ShellLinkNotCreated); DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =("); return false; } if (FAILED(DllImporter::SetCurrentProcessExplicitAppUserModelID(_aumi.c_str()))) { setError(error, WinToastError::InvalidAppUserModelID); DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =("); return false; } _isInitialized = true; return _isInitialized; } bool WinToast::isInitialized() const { return _isInitialized; } const std::wstring& WinToast::appName() const { return _appName; } const std::wstring& WinToast::appUserModelId() const { return _aumi; } HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) { WCHAR path[MAX_PATH] = { L'\0' }; Util::defaultShellLinkPath(_appName, path); // Check if the file exist DWORD attr = GetFileAttributesW(path); if (attr >= 0xFFFFFFF) { DEBUG_MSG("Error, shell link not found. Try to create a new one in: " << path); return E_FAIL; } // Let's load the file as shell link to validate. // - Create a shell link // - Create a persistant file // - Load the path as data for the persistant file // - Read the property AUMI and validate with the current // - Review if AUMI is equal. ComPtr shellLink; HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); if (SUCCEEDED(hr)) { ComPtr persistFile; hr = shellLink.As(&persistFile); if (SUCCEEDED(hr)) { hr = persistFile->Load(path, STGM_READWRITE); if (SUCCEEDED(hr)) { ComPtr propertyStore; hr = shellLink.As(&propertyStore); if (SUCCEEDED(hr)) { PROPVARIANT appIdPropVar; hr = propertyStore->GetValue(PKEY_AppUserModel_ID, &appIdPropVar); if (SUCCEEDED(hr)) { WCHAR AUMI[MAX_PATH]; hr = DllImporter::PropVariantToString(appIdPropVar, AUMI, MAX_PATH); wasChanged = false; if (FAILED(hr) || _aumi != AUMI) { // AUMI Changed for the same app, let's update the current value! =) wasChanged = true; PropVariantClear(&appIdPropVar); hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar); if (SUCCEEDED(hr)) { hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar); if (SUCCEEDED(hr)) { hr = propertyStore->Commit(); if (SUCCEEDED(hr) && SUCCEEDED(persistFile->IsDirty())) { hr = persistFile->Save(path, TRUE); } } } } PropVariantClear(&appIdPropVar); } } } } } return hr; } HRESULT WinToast::createShellLinkHelper() { WCHAR exePath[MAX_PATH]{L'\0'}; WCHAR slPath[MAX_PATH]{L'\0'}; Util::defaultShellLinkPath(_appName, slPath); Util::defaultExecutablePath(exePath); ComPtr shellLink; HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); if (SUCCEEDED(hr)) { hr = shellLink->SetPath(exePath); if (SUCCEEDED(hr)) { hr = shellLink->SetArguments(L""); if (SUCCEEDED(hr)) { hr = shellLink->SetWorkingDirectory(exePath); if (SUCCEEDED(hr)) { ComPtr propertyStore; hr = shellLink.As(&propertyStore); if (SUCCEEDED(hr)) { PROPVARIANT appIdPropVar; hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar); if (SUCCEEDED(hr)) { hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar); if (SUCCEEDED(hr)) { hr = propertyStore->Commit(); if (SUCCEEDED(hr)) { ComPtr persistFile; hr = shellLink.As(&persistFile); if (SUCCEEDED(hr)) { hr = persistFile->Save(slPath, TRUE); } } } PropVariantClear(&appIdPropVar); } } } } } } return hr; } INT64 WinToast::showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHandler* handler, _Out_ WinToastError* error) { setError(error, WinToastError::NoError); INT64 id = -1; if (!isInitialized()) { setError(error, WinToastError::NotInitialized); DEBUG_MSG("Error when launching the toast. WinToast is not initialized."); return id; } if (!handler) { setError(error, WinToastError::InvalidHandler); DEBUG_MSG("Error when launching the toast. Handler cannot be null."); return id; } ComPtr notificationManager; HRESULT hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); if (SUCCEEDED(hr)) { ComPtr notifier; hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); if (SUCCEEDED(hr)) { ComPtr notificationFactory; hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), ¬ificationFactory); if (SUCCEEDED(hr)) { ComPtr xmlDocument; HRESULT hr = notificationManager->GetTemplateContent(ToastTemplateType(toast.type()), &xmlDocument); if (SUCCEEDED(hr)) { const int fieldsCount = toast.textFieldsCount(); for (int i = 0; i < fieldsCount && SUCCEEDED(hr); i++) { hr = setTextFieldHelper(xmlDocument.Get(), toast.textField(WinToastTemplate::TextField(i)), i); } // Modern feature are supported Windows > Windows 10 if (SUCCEEDED(hr) && isSupportingModernFeatures()) { // Note that we do this *after* using toast.textFieldsCount() to // iterate/fill the template's text fields, since we're adding yet another text field. if (SUCCEEDED(hr) && !toast.attributionText().empty()) { hr = setAttributionTextFieldHelper(xmlDocument.Get(), toast.attributionText()); } const int actionsCount = toast.actionsCount(); WCHAR buf[12]; for (int i = 0; i < actionsCount && SUCCEEDED(hr); i++) { _snwprintf_s(buf, sizeof(buf) / sizeof(*buf), _TRUNCATE, L"%d", i); hr = addActionHelper(xmlDocument.Get(), toast.actionLabel(i), buf); } if (SUCCEEDED(hr)) { hr = (toast.audioPath().empty() && toast.audioOption() == WinToastTemplate::AudioOption::Default) ? hr : setAudioFieldHelper(xmlDocument.Get(), toast.audioPath(), toast.audioOption()); } if (SUCCEEDED(hr) && toast.duration() != WinToastTemplate::Duration::System) { hr = addDurationHelper(xmlDocument.Get(), (toast.duration() == WinToastTemplate::Duration::Short) ? L"short" : L"long"); } } else { DEBUG_MSG("Modern features (Actions/Sounds/Attributes) not supported in this os version"); } if (SUCCEEDED(hr)) { hr = toast.hasImage() ? setImageFieldHelper(xmlDocument.Get(), toast.imagePath()) : hr; if (SUCCEEDED(hr)) { ComPtr notification; hr = notificationFactory->CreateToastNotification(xmlDocument.Get(), ¬ification); if (SUCCEEDED(hr)) { INT64 expiration = 0, relativeExpiration = toast.expiration(); if (relativeExpiration > 0) { MyDateTime expirationDateTime(relativeExpiration); expiration = expirationDateTime; hr = notification->put_ExpirationTime(&expirationDateTime); } if (SUCCEEDED(hr)) { hr = Util::setEventHandlers(notification.Get(), std::shared_ptr(handler), expiration); if (FAILED(hr)) { setError(error, WinToastError::InvalidHandler); } } if (SUCCEEDED(hr)) { GUID guid; hr = CoCreateGuid(&guid); if (SUCCEEDED(hr)) { id = guid.Data1; _buffer[id] = notification; DEBUG_MSG("xml: " << Util::AsString(xmlDocument)); hr = notifier->Show(notification.Get()); if (FAILED(hr)) { setError(error, WinToastError::NotDisplayed); } } } } } } } } } } return FAILED(hr) ? -1 : id; } ComPtr WinToast::notifier(_In_ bool* succeded) const { ComPtr notificationManager; ComPtr notifier; HRESULT hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); if (SUCCEEDED(hr)) { hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); } *succeded = SUCCEEDED(hr); return notifier; } bool WinToast::hideToast(_In_ INT64 id) { if (!isInitialized()) { DEBUG_MSG("Error when hiding the toast. WinToast is not initialized."); return false; } const bool find = _buffer.find(id) != _buffer.end(); if (find) { bool succeded = false; ComPtr notify = notifier(&succeded); if (succeded) { notify->Hide(_buffer[id].Get()); } _buffer.erase(id); } return find; } void WinToast::clear() { bool succeded = false; ComPtr notify = notifier(&succeded); if (succeded) { auto end = _buffer.end(); for (auto it = _buffer.begin(); it != end; ++it) { notify->Hide(it->second.Get()); } } _buffer.clear(); } // // Available as of Windows 10 Anniversary Update // Ref: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts // // NOTE: This will add a new text field, so be aware when iterating over // the toast's text fields or getting a count of them. // HRESULT WinToast::setAttributionTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text) { Util::createElement(xml, L"binding", L"text", { L"placement" }); ComPtr nodeList; HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList); if (SUCCEEDED(hr)) { UINT32 nodeListLength; hr = nodeList->get_Length(&nodeListLength); if (SUCCEEDED(hr)) { for (UINT32 i = 0; i < nodeListLength; i++) { ComPtr textNode; hr = nodeList->Item(i, &textNode); if (SUCCEEDED(hr)) { ComPtr attributes; hr = textNode->get_Attributes(&attributes); if (SUCCEEDED(hr)) { ComPtr editedNode; if (SUCCEEDED(hr)) { hr = attributes->GetNamedItem(WinToastStringWrapper(L"placement").Get(), &editedNode); if (FAILED(hr) || !editedNode) { continue; } hr = Util::setNodeStringValue(L"attribution", editedNode.Get(), xml); if (SUCCEEDED(hr)) { return setTextFieldHelper(xml, text, i); } } } } } } } return hr; } HRESULT WinToast::addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& duration) { ComPtr nodeList; HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); if (SUCCEEDED(hr)) { UINT32 length; hr = nodeList->get_Length(&length); if (SUCCEEDED(hr)) { ComPtr toastNode; hr = nodeList->Item(0, &toastNode); if (SUCCEEDED(hr)) { ComPtr toastElement; hr = toastNode.As(&toastElement); if (SUCCEEDED(hr)) { hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), WinToastStringWrapper(duration).Get()); } } } } return hr; } HRESULT WinToast::setTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text, _In_ int pos) { ComPtr nodeList; HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList); if (SUCCEEDED(hr)) { ComPtr node; hr = nodeList->Item(pos, &node); if (SUCCEEDED(hr)) { hr = Util::setNodeStringValue(text, node.Get(), xml); } } return hr; } HRESULT WinToast::setImageFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path) { wchar_t imagePath[MAX_PATH] = L"file:///"; HRESULT hr = StringCchCatW(imagePath, MAX_PATH, path.c_str()); if (SUCCEEDED(hr)) { ComPtr nodeList; HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"image").Get(), &nodeList); if (SUCCEEDED(hr)) { ComPtr node; hr = nodeList->Item(0, &node); if (SUCCEEDED(hr)) { ComPtr attributes; hr = node->get_Attributes(&attributes); if (SUCCEEDED(hr)) { ComPtr editedNode; hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode); if (SUCCEEDED(hr)) { Util::setNodeStringValue(imagePath, editedNode.Get(), xml); } } } } } return hr; } HRESULT WinToast::setAudioFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path, _In_opt_ WinToastTemplate::AudioOption option) { std::vector attrs; if (!path.empty()) attrs.push_back(L"src"); if (option == WinToastTemplate::AudioOption::Loop) attrs.push_back(L"loop"); if (option == WinToastTemplate::AudioOption::Silent) attrs.push_back(L"silent"); Util::createElement(xml, L"toast", L"audio", attrs); ComPtr nodeList; HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"audio").Get(), &nodeList); if (SUCCEEDED(hr)) { ComPtr node; hr = nodeList->Item(0, &node); if (SUCCEEDED(hr)) { ComPtr attributes; hr = node->get_Attributes(&attributes); if (SUCCEEDED(hr)) { ComPtr editedNode; if (!path.empty()) { if (SUCCEEDED(hr)) { hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode); if (SUCCEEDED(hr)) { hr = Util::setNodeStringValue(path, editedNode.Get(), xml); } } } if (SUCCEEDED(hr)) { switch (option) { case WinToastTemplate::AudioOption::Loop: hr = attributes->GetNamedItem(WinToastStringWrapper(L"loop").Get(), &editedNode); if (SUCCEEDED(hr)) { hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); } break; case WinToastTemplate::AudioOption::Silent: hr = attributes->GetNamedItem(WinToastStringWrapper(L"silent").Get(), &editedNode); if (SUCCEEDED(hr)) { hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); } default: break; } } } } } return hr; } HRESULT WinToast::addActionHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& content, _In_ const std::wstring& arguments) { ComPtr nodeList; HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"actions").Get(), &nodeList); if (SUCCEEDED(hr)) { UINT32 length; hr = nodeList->get_Length(&length); if (SUCCEEDED(hr)) { ComPtr actionsNode; if (length > 0) { hr = nodeList->Item(0, &actionsNode); } else { hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); if (SUCCEEDED(hr)) { hr = nodeList->get_Length(&length); if (SUCCEEDED(hr)) { ComPtr toastNode; hr = nodeList->Item(0, &toastNode); if (SUCCEEDED(hr)) { ComPtr toastElement; hr = toastNode.As(&toastElement); if (SUCCEEDED(hr)) hr = toastElement->SetAttribute(WinToastStringWrapper(L"template").Get(), WinToastStringWrapper(L"ToastGeneric").Get()); if (SUCCEEDED(hr)) hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), WinToastStringWrapper(L"long").Get()); if (SUCCEEDED(hr)) { ComPtr actionsElement; hr = xml->CreateElement(WinToastStringWrapper(L"actions").Get(), &actionsElement); if (SUCCEEDED(hr)) { hr = actionsElement.As(&actionsNode); if (SUCCEEDED(hr)) { ComPtr appendedChild; hr = toastNode->AppendChild(actionsNode.Get(), &appendedChild); } } } } } } } if (SUCCEEDED(hr)) { ComPtr actionElement; hr = xml->CreateElement(WinToastStringWrapper(L"action").Get(), &actionElement); if (SUCCEEDED(hr)) hr = actionElement->SetAttribute(WinToastStringWrapper(L"content").Get(), WinToastStringWrapper(content).Get()); if (SUCCEEDED(hr)) hr = actionElement->SetAttribute(WinToastStringWrapper(L"arguments").Get(), WinToastStringWrapper(arguments).Get()); if (SUCCEEDED(hr)) { ComPtr actionNode; hr = actionElement.As(&actionNode); if (SUCCEEDED(hr)) { ComPtr appendedChild; hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild); } } } } } return hr; } void WinToast::setError(_Out_ WinToastError* error, _In_ WinToastError value) { if (error) { *error = value; } } WinToastTemplate::WinToastTemplate(_In_ WinToastTemplateType type) : _type(type) { static const std::size_t TextFieldsCount[] = { 1, 2, 2, 3, 1, 2, 2, 3}; _textFields = std::vector(TextFieldsCount[type], L""); } WinToastTemplate::~WinToastTemplate() { _textFields.clear(); } void WinToastTemplate::setTextField(_In_ const std::wstring& txt, _In_ WinToastTemplate::TextField pos) { _textFields[pos] = txt; } void WinToastTemplate::setImagePath(_In_ const std::wstring& imgPath) { _imagePath = imgPath; } void WinToastTemplate::setAudioPath(_In_ const std::wstring& audioPath) { _audioPath = audioPath; } void WinToastTemplate::setAudioOption(_In_ WinToastTemplate::AudioOption audioOption) { _audioOption = audioOption; } void WinToastTemplate::setDuration(_In_ Duration duration) { _duration = duration; } void WinToastTemplate::setExpiration(_In_ INT64 millisecondsFromNow) { _expiration = millisecondsFromNow; } void WinToastTemplate::setAttributionText(_In_ const std::wstring& attributionText) { _attributionText = attributionText; } void WinToastTemplate::addAction(_In_ const std::wstring & label) { _actions.push_back(label); } std::size_t WinToastTemplate::textFieldsCount() const { return _textFields.size(); } std::size_t WinToastTemplate::actionsCount() const { return _actions.size(); } bool WinToastTemplate::hasImage() const { return _type < WinToastTemplateType::Text01; } const std::vector& WinToastTemplate::textFields() const { return _textFields; } const std::wstring& WinToastTemplate::textField(_In_ TextField pos) const { return _textFields[pos]; } const std::wstring& WinToastTemplate::actionLabel(_In_ int pos) const { return _actions[pos]; } const std::wstring& WinToastTemplate::imagePath() const { return _imagePath; } const std::wstring& WinToastTemplate::audioPath() const { return _audioPath; } const std::wstring& WinToastTemplate::attributionText() const { return _attributionText; } INT64 WinToastTemplate::expiration() const { return _expiration; } WinToastTemplate::WinToastTemplateType WinToastTemplate::type() const { return _type; } WinToastTemplate::AudioOption WinToastTemplate::audioOption() const { return _audioOption; } WinToastTemplate::Duration WinToastTemplate::duration() const { return _duration; } spectral/src/spectraluser.h0000644000175000000620000000057213566674120016042 0ustar dilingerstaff#ifndef SpectralUser_H #define SpectralUser_H #include "room.h" #include "user.h" #include using namespace QMatrixClient; class SpectralUser : public User { Q_OBJECT Q_PROPERTY(QColor color READ color CONSTANT) public: SpectralUser(QString userId, Connection* connection) : User(userId, connection) {} QColor color(); }; #endif // SpectralUser_H spectral/src/imageclipboard.h0000644000175000000620000000105213566674120016262 0ustar dilingerstaff#ifndef IMAGECLIPBOARD_H #define IMAGECLIPBOARD_H #include #include #include class ImageClipboard : public QObject { Q_OBJECT Q_PROPERTY(bool hasImage READ hasImage NOTIFY imageChanged) Q_PROPERTY(QImage image READ image NOTIFY imageChanged) public: explicit ImageClipboard(QObject* parent = nullptr); bool hasImage() const; QImage image() const; Q_INVOKABLE bool saveImage(const QUrl& localPath); private: QClipboard* m_clipboard; signals: void imageChanged(); }; #endif // IMAGECLIPBOARD_H spectral/src/messageeventmodel.h0000644000175000000620000000366513566674120017043 0ustar dilingerstaff#ifndef MESSAGEEVENTMODEL_H #define MESSAGEEVENTMODEL_H #include "room.h" #include "spectralroom.h" #include class MessageEventModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(SpectralRoom* room READ room WRITE setRoom NOTIFY roomChanged) public: enum EventRoles { EventTypeRole = Qt::UserRole + 1, MessageRole, EventIdRole, TimeRole, SectionRole, AuthorRole, ContentRole, ContentTypeRole, HighlightRole, ReadMarkerRole, SpecialMarksRole, LongOperationRole, AnnotationRole, UserMarkerRole, ReplyRole, ShowAuthorRole, ShowSectionRole, BubbleShapeRole, ReactionRole, // For debugging EventResolvedTypeRole, }; enum BubbleShapes { NoShape = 0, BeginShape, MiddleShape, EndShape, }; explicit MessageEventModel(QObject* parent = nullptr); ~MessageEventModel() override; SpectralRoom* room() const { return m_currentRoom; } void setRoom(SpectralRoom* room); int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; Q_INVOKABLE int eventIDToIndex(const QString& eventID) const; private slots: int refreshEvent(const QString& eventId); void refreshRow(int row); private: SpectralRoom* m_currentRoom = nullptr; QString lastReadEventId; int rowBelowInserted = -1; bool movingEvent = 0; int timelineBaseIndex() const; QDateTime makeMessageTimestamp( const QMatrixClient::Room::rev_iter_t& baseIt) const; QString renderDate(QDateTime timestamp) const; void refreshLastUserEvents(int baseRow); void refreshEventRoles(int row, const QVector& roles = {}); int refreshEventRoles(const QString& eventId, const QVector& roles = {}); signals: void roomChanged(); }; #endif // MESSAGEEVENTMODEL_H spectral/icons/0002755000175000000620000000000013566674120013477 5ustar dilingerstaffspectral/icons/icon.icns0000644000175000000620000005020713566674120015307 0ustar dilingerstafficnsPit32wƬŘoSIOjvOIKmĄOIKxꁟI_lI[pIlIJJIcqIOҮIIdItItϳIIIIOƶIaӷIIJ_ImIċθI[tI_uMIaIOIxIIƍJIxJIIXILIWSIoIdInKIe̙fIKIܩ\IIVߺqPImIղmNIjNIѮfKIOچIKǞoLIߧIL軅QIħIJ~KIIޛXIIZWIIx֫MIIL׫tIIJLIIaީXIIJkק_II]dII]]IšIJcOIާI_IۧOIQyՖ|IOjIhޓPImIJv˒IIw̐RIKIIKmIa׎ՁIqITюhILIVݍIIKxIjIIxINIaIVm_I[IrIKmIJIIdIOIIIItItIO큮IvIOIXgIJIqlIW[ISaI\yLIJznKIJr返jOIMnīֺƤխ잢۪ޮ첈ܳҴ͵εֶ߷귈ȰՋ۸ǸȮ񕤌ߊ҈Ƹέ֍版ґÀ칎ūʐƙ뉁ں΃ɕūҩϸʵDZㆈͤ׽ӇͧЭէƉЩƧ弑ҫ⣍ْ֫㖉癈ŝ敇ǧ֡ߌӧ槈Ɇ񌪈挃ْҳڐ뎂Ω쉴߁Ķݎī芶ڈθÌҭ񗷈ȸƌƯȷ񞷈݉ߌƴ񬲈죯﫮񴭈njᛩܞȔȗΦşҷƾĽԶĠԩ왢ڪܮ첂۳д˵̵նݷ鷂ŰԋڸŴƮ񐟌݄Ђø̭Ս惂Ќ춈«ȐÔ냁ٷ̃ƕ§ЩʹȱŮↂˤպч˧ΪԧĉΥħ幌Ы߫៍ի،⑉甈Ý叇ŧԡ݆ѧ姂dž񆪂ߖ冃ؒгِꈂ̩냴ށ¶܎脶ق̸Э񒷂ƸČįŷ񙷂܃ކĴ񨲂잯燐񱭂ņۙŎƒ̢šгût8mk@_ʟ_<:" YVw~zYU"9;`]Ȟ`\8: WRzvoUR97]ɜ]spectral/icons/hicolor/0002755000175000000620000000000013566674120015136 5ustar dilingerstaffspectral/icons/hicolor/256-apps-spectral.png0000644000175000000620000002006513566674120020735 0ustar dilingerstaffPNG  IHDR\rfsBIT|d pHYs>>kktEXtSoftwarewww.inkscape.org<IDATxyx[՝7dyb;ǎkI@B:δOa:hita+әvڗNvЄ%!$B I8}-GK$[t'nxWGyl}usϑ$9f k4X3XQBHp ]@ P@ pb4#Ԇczӹ&+*ZP=vƽ$ )"*e}1v/'$ J3rnXj `%`d=e6ȱ)Q/^h~ ` y!m &]cqS A8I܌'F'{`R`ٲt:KNY5L.ñX&\Bbl2LoLI5ЛP`D4p81~=BJ{p<;'l `%p|b<9co!lX.{ OubS 򥾓>@SkXG᨟쌱NЛYZvƘ~Z.1KE NC˻g1|>!D$Ir|nt QXV߿fn/$I !1l65{$}/!"I|}_\.Wi 8̃5c$^Yhd 'D$_ 0"Ng;h?BAhL$iHNvЛpt:;1 !$6+Bo%HGeBH9VwBHt G ?5NAbFNA >X1F~,)!\4SB0jDNA"&$Qƨ cT1*B!a 0(9c! ``p"#"h1z= QQ0EG#FOCDԎ @>m}h﷡aGÎ=Nx}ImW#I0Ej0 dFшDxd[0DF_BMr8w2~>=8юn4b?,X2PT^`B\1tv g[Q5OtI K~ܷWuT+ @F*yG Ov ;JHɐߏARف܁.SQ|GAvwD ?t]t_wڮNQBL*xG c48F'iC χ& ynϼ ݼ *_΂p{xjNxG T2;OIS @T±]r7v5(+rLB~=yG @!KT:}38T2@B;X}{N~;T2=t䣰E@"oݤ"T2kibJ5վј*#"xG 29цO#hdPWACOx A;/;,fA"-^:m611G $9YDdcM+UT}U @1z=-cvwgxP L SxG qjw\B7vP A*e ӧt!;@U[+t嵉>^=UUvCnl&YMxd9zKLBqz& 0>06n9-X^՜WRc)7NO~sc^(S'xǐ* `V=1 wK?st D9\8 ww Y^8w/l;+Wi\se a:FUNE +LIŃKWp@v)U%|hGco ?!͵Κ,(iL"*|{b rm3/[Puaʄ.A uTsc |2xxǘ xCȥy}>hjrO73-Lq*qw)>0Ռ`3FE׬ۆk"8DGjxGa 90-Mc>DSz9_ MQj:v?yzd'P|G./1i1` T~7I`:~zx(JKLB1 Γ!llj?{&w0<l/{; pe\u }A.פ[+oۀeEnBg{HȌEE;'tuLafiXyq kqvw:3Q `^^vu h&WKԵz#9 kGjpȜ*4h5%&!/1i{^=F]w'{oSŝl1ڮNff2!@ugǔCӡ0%)fjACO7꺻K v*uwN]|?%71 Oyn/$HK sӄ~Wy}>;s*1+#J wtZ-haA巠8#Ͽ.\\r¼*D1nifFFbI~!֖FD3?'ݷ)' 9ɩȵ& uʢb8<ϑ~@@c\NC5 /cMD^br Ke{x۶P('dee 5|#tȌO@^tyIȌO@)]rjbM;BC]w5j5YkM´d&&!71IU9Ir z&tiqٶ BfjCod`FFJ!/ixoaZRr|޶{ul7Q((#>wU 0~m%%bVęf NP `I26a׉.lX Fl[/:8m'Ș(8+TT4A.u:veޮ@9+bz#o/ϣgRO-R;xxOڍVޑ&DEK' @=H$J1&QXk"= 1:qnv.SP94LKLV(rB7r}$2,t9P={[_Dϲ L9 VT%\@Iz&dm[_A}O78c”jB%cfg@&?M钛*d*DYB@qz"0`U%l{h(\smQ0=P!du:AaO^{+OrV[fcK3~4b0?gD_|!6ggk`S¼|HBƶϽzH-b>Fy~ [XHMȨ ?߻'֛;43(q'`I~!Df-Mվ!3k^Nޘ?_V8=HI!t,͟NWTX}-^-?;RqL .J0FEݴ^)?иkNvΨ_;M'tc{ (:%ųD~,XiPU{яy@(W13-Cy _Imųy WUށJJ9 ~,FFÎcx@UP#EG9aD<5C^bȿvfjSPE9ihd*1{N}5C:6EN)s(H] <^/O1 /Jٙ%-:l]Hiy}>hj)qu:l*-TnI1a9ѺnK/7 `hiB=JQ]$MX_79Kk"U!S9TYQغ`1DuTrRe%ڙ.wUQmH/Ya'k$q9xGPU;P6w "> 2Lt.``:-%1=n1TC5렣J3N$5D&CC#FXlE2K()()$kko%nO+׫rHg8ٟdv ܻhd bzT# 6ΙG  5ĸ$IB>w 'xxtF]덈!!@=(l QΆ-te@ i*l9u@Ɍl sL (dhM^a_w7lH$*TZ qJRl St4(d #)3*&&[ng5;P"mF'BLYVC)LfSE&uQMZ4(G$a1Tiu,Mifn#hߠ.o:.~V>l-n WU pWʢbQG1nmƁsU(oP#i(cM'Q\hcH SӰ`;@H辄8^_ ^Jj13= r1/'4Zq^g[Q܄3m-hށ$INHDqzJ2213-8*x^Twu5]hq;hE5 )$ 6  \D6zv8qr7ɱIB\l,#FRqȰ$ #>*@<^/\kpDEGCd-heMof2 zEH $Qƨ cT1*B!aL`wB;#!͡❂…S#IR.l@ P;!$$I;! y pqA?杂|>@.<1LWFl ~cBH1^kc,teZQnј!IҐ$I$i;TI$ICwf&D~w勑0gᑈ4F+_\3c!K x꯯)| &"+qqqW|>W\ Z$B$IrG  頤"cGK`+eJ#(h\qFT$NMBl~Ĵx_`ec7ڮcN n2ސ$sBl;cn.h|1#bB&Ifonp<)"OM&wq8qĺɤ"(/I#F>aBc ~%(1͎7۝^ $X,Ɖ>qRL"$IcLv;IF$&IZ_|n"+:Lڔ 5;|rm2x1wNlp,E}9 C$I/ՓyA96X`RE x1d23cQgٲ"""`:cl+`e!$ pp$]jzl?^ H̋:RIENDB`spectral/icons/hicolor/32-apps-spectral.png0000644000175000000620000000203613566674120020643 0ustar dilingerstaffPNG  IHDR szzsBIT|d pHYsbtEXtSoftwarewww.inkscape.org<IDATXŗO[e?+XVJkL:R~2Dԁxś]p?`b&^M.1FbX)Is8J[PzJ)-^8;%e|{yO}([>*ao$=ݞGӴpt _IZeM.CqjM.HT*޷};dOHRQf묠b)&d.=lڸքʯ?;>F6 u?Ŭ͜nj_Ç=WI&P58jr/["LD,tsҍN_a˯7_V8l=h[5kMӘ19v3No Qo>;>&^HWN>V{ڏjqr.gc,Ƴ3pdEta~wZEptwjػfpW*8TNqNïcTdTOD %ťmڤ 2烝돕`]+73DXL+*irR [4<PBT\ٯB˒(BaןZeݞ$U0 !>ZG_ =IENDB`spectral/icons/hicolor/128-apps-spectral.png0000644000175000000620000000767213566674120020744 0ustar dilingerstaffPNG  IHDR>asBIT|d pHYs>ZtEXtSoftwarewww.inkscape.org<7IDATx{P[םǿGHy%K2`+ĉ[oivivvf;vɦ&MfؙNb'bCc7SW$$pm@ 3?9_|!Xn;RNV.hPr` |P@ 1t:><U*l)1`hr=Bo e B8НT7x !\H@G)}r˝޼n9/V (<7f0u? .9{s]rۏtx߿`0t3*P.R@<sFbN<#16n7ȂeYE)r"Jb+QBN&W&E*L.*,MT\WXUP92w\ TX:(X(X,34Qgc/((bZiЪ5l0 =c| *mRD>=DC zՖVŐUf VY2lA6:̊Ņ&AvJ*`kNqqLDec=>j2賡"%(/(B\d'&,@ CŚصZ3׋ !HJHKqwal2FӍʔT<^v7V-e^de|Xwk/-#(!m-C 5;xO&(*9WSpdcߟ?ܪf34gbK#0@{υëgOW΀RiXl&p ^~&~D)Qmmƿ_ybK y -#ۺw 7@РPoN}L y -fJQ5HbK`ɺ+SD@l ΢)Jߒ7+x}>o%5_P~%o%k_於XbJ)~uSCe[`88P'X-/  1@e5\j_礦Ȳ,|$0>lj/P(䤈mJOp0`?qGn7B-7&ۗN1k5d=37+v섘q_꺻eS\hiԜ5+6"N-[3탩oϏ|Kx8nَ_| /+ŐvFF3KXx;/,0l|1 +^klKފ/6V^] pӘ~T6V#ґG6owRG{lhv?kB =6lXͤ}fkB6X ],{JbOZ@;7Ⱓ r-?4;n{| =oM  60Ͳ DkV}JٞZO7oԴ/bﺍ,$̋dM0`kVVN;lD68J)L &^]cm,d,J,S L)dNiGC W:`u:002 cB`b`SgNdLp+1 ,(dlƨ׋NڂVҸl1@;?+ '¾ X +qZ-JsPχ˝8ԀKLvVgd]&p-,|IQs}]bU);svTCN.AHbV[R{kqxx~<宰Oq=>6&%?82NA yj5ۀWaQQaiwX1ف??,7h4xt+~WPd1L @8t KvUB̉<m !2˙ c=RԴ M"l+y Ekm5!zu7_.n}6`j}DkǡdY&Je0݂i(J'}_Ŀ{{AeGt"lsqg&Lt䦦#;e2δ sG4KgfDۍ>2!H'"+9+L)JNFvr*)nLt^|AMhL (nRA )Y)O3l S`5 r~UU%Qi>nN73?FAAf 6FFiE:gSV#csB`ILBHU/Ba|b;qWWa1r-/ٴruwΚi41+1 ^ejZP:{qOo_ 3TnLPf禦i 𧔠j;ܑC;q>{bzβ!?־ffKxՉggqRJBt n?}4 + Nl ZoHSih3KKGB4;l^]CxA>0ܘJY[sBZ #ژIi`zuqq(^Yp9cxzH%2WIc,/(b݅dhu|MVr 4SYDKքC֜07@\Yw#\sH[4QQXnJF YaTvg@E>/1pl',3$aﺍLݕ_(yoiCsP,; yt bMTX*DWFOH;F9݅ȉ4Jau^#X fBcl Ayu#h&+]NL*{lkYtT V㛻+~6 RL _Z&Fׂe#܈Rsoj.^-V̉)xTrlS39yl[",s</kԞeZ2 znj1O~4lf900y'xrw+fΚSud L./+;vt+8f-#h$ei)*}FR̗6my%i`r3 | RAYupAx|ӁΝXՅŋ}I5O uxb Ŗ)x  ˔"#LqG?kHl90&pxs 0MRXQx ݝW&`mr]ʷl 0([z#N̈́%'r`&" 0Q =6ۺp+eJFAf ̲)d,g[ݰ k&0ډj"Z>.iz=0&p}7H6 88*_V` @e- . hS[hԩ[8PJ+NHPL08pBl5 s\RjGT HW8&!93"~I(17 `0 ! lFࡄǓ:_Zq|OpY B qgpbNPN{7fMwqQJ_""<~7Y9{p4|kf@5 yi1:^G)^_3߇R@1,L>"Q'`rRzrBst$IENDB`spectral/icons/hicolor/512-apps-spectral.png0000644000175000000620000004225313566674120020733 0ustar dilingerstaffPNG  IHDRxsBIT|d pHYs}}+tEXtSoftwarewww.inkscape.org< IDATxw|[a 8=%"%-yɶ,w4qf7m3&j&iդmӷۦ:vOyoYlړD HVh.~>$'6E<8s,9۶sl۞cveYfH*?i<DCAIݒ$uH:bveYuZ˲Bs:2 l'۶/,km $)Öem{-ژײ`<99z۶tәqGҋzϤ7t.bxI"by8~188a& T۶e}Pt `e(++t3qmItt>",qIh:̩]dY?neY_r"@ P-n1>~;8^l۾L '{=tDqemےrN'~y@4ض}p8 J.~_d$ylZ5t lN|۶NfǶj)@ض]mt=cYLA@ض]Tf:۶= LQo:˲ )@Tx$yMQ1DD QH@D QH@D QH@D QH@^PHCR`hP! jxtL40:|ycc 5#ا`@ )04P(|>e)/#S,eeW?K,dMJz.ct ٶ@:թ^#PȰxSbYr32Ty*/Py~$< e#:U١]:١CП㥠T,ӱB;8mڬ}-:֪P8l:ghVafdN;6@QkfmnҾ&LGIII,,RuI (9 Y[ MGK>WJir-.+WEa,ӡF-Ч-봥67il|tSkZR>SfV*`R(նR_qp׫ehdӑףgӭkkځjM?&^-^Uպr6A@WP׆5l7jIL]1W{!p 6K{wkKKr3trM50׫ҋ{v)8;a* tytY< aQpB:TvԞF>'d;_k-.^,WFJ8@PgizQ'Ruݢ%u N@#j4q(ӍK%˔ vQWtĹt_n^B7-YbA$ FgPx䷬GTefiy eYPA@\{@]AQKKWt`B(yQ-YBEY٦gD@L RMk(q>W7.Yp- b-= R?Kr-)i: p b֣[6Mʪj}lNK78A~am~|Х{ĜА{81i5? (9{1]t `|^8bݲl{d<{'oyQF` 1opP΢?ďm 譺 Q3~U DTpxHؘ8H ĄMp`c՗Wm APz^MVEk>ݲt$ \Tf:~S LA¶d:uWOGLGA6  [_{~m:xt! \mݎ#F GϬ/׿P8l:р.}j6}w#5qzz6Wؠ7qW /p CvQ(p kl|t zʹ W4vu'=0{$ \axKoh:W`RG70QB+MG-_|VM=ݦ (pPؘ~cZ.Q enϛQ $N~=n t 8W%S sO NQ Llxt 8WHIN6ig6?@ܡȩqW𧥚 C \ȟm:ؠg50nP i>2RRLp^}c (pBF /?T@5 ~L#zu1p(p 3~U s@kg瘎`cP3 LG0 kj10EFeA8-5 )5|> 4S@+f:IZ}:10Ii3LG0Iw10Ie#t LRWY^&ɖ?^aB묨e:)mkMQ:T6k@..Q 1?'o1@pP8n1ʒtY<1L[8 ˽wv55uW;O}X48:u۷Xh|\lyK 6 I,-g1 ܾE#^ػK{{LG1EKLG0EzrnFpPM=Y(b1LFpV$?bLqGkQ\(񭛏ٖTa.AfVt S䎭6@p?w%f$}~ 6 ꙝM)P\ƶm=z§ww$fR]2g];46>n:e6ґmo5=>rjSSM0CpuN`;Ѳˮ0=} ' E:ڵ_?k{U<]<{S4P\lfH Iee` d{`WD^9s4RJ(k: Iz.7s3 K<{YWֶjxl,JܫHzY[;c(.moY ;O 1bos]`(lOn{J.ZO#$< aUwЁ$oU<}wO^޷G `PMk{{&}Z?EH߹\76#Q zfjC`JSHIY^V))81!Q kYv; ]:g~rtEby >[ihttE0d[CS?Hw;;"(>eޮ|q`4Җuc$, !;^alL/w>:ed7՚(GFRs,sG>2g$ #2502RsR^ݲ|ۇ>ON9# k461`zn׎]+xt?\s LGҦLGHHleᰶ\=֪mS\k&$G͙KӖCzz6<({;4-ǦR35vD,I+*fk7W?c7 {ФDAdPmp{h4Qr-u%W:™y,KE,,ҭWpxH4jWSSw鈀BҬe妣$ @8=[=_շn?Gຌ?5M+Z.mi&mnRgtDG(QBOtUӪyQ{ML%iF^f$I]AkifmiRcwSH85ͦ#$ @m[5-Q}_oxUfV(3%59su霹Y5-:ުC6p֡vMJ2%QpgGW ލW^EdhJ-Yy~u]tU!) T&Q@}^ڻKyfy}D^^F22bk>zuM:TѮC9n0(bؒ~sVjr p%$;G%9ǧlVsou`[vCٰhktG?f?^'`* QXyU%]}tM:N#P F>4% CC =l.=h,,/_=Z a5loSh|pZ$~u9aԸ򢪊K8%y<'P::ѮnGDմ4شApXm{~:};z5t枰ؘ;;yPٮ.N@tqC.ڶVݷ5}զ &'k^wm/=8:w#]S"4vu(;a:»<}f_1LEwR)СOoh`JIJ (Sg0`:ƻؒT}܌ Șu|ƵmGv!&#0;#.' O=, 3YUk۸]mmovDĖQ!}gݣTFJ8H EY*ʪjIR(մhk6sT2$I ]l $ ܽ(SY~yQ^G5Xk/$u|D;4pJߡj6jZ[g"c: I*tDHnh8࠶>&dzkRJ~]>w }ؠm y!m":Zzq!8Mȶm}(pl/X,RضUڢ-iGc;]3ЯP8$PT(6cR8T?>wZv DX= >pǶכjH=m[*g@ @Yv3cJ(Yi>(9юzNo2U۵`zqbcTvi7rmNO7ʪjh:yVjbBG M Cc|r]G՛ߣҜ\qsst9 v!v4fL(Lڃ/]{ 5zfe88ɬohP|Хk.u WKv%:)N8$F1_VSO>zj@*\V\-]pXaqSÔQOڡƮN}i:ǣ gUYU z~N pC#-nt?514U׼W ޅVA=cjZ[LI{Jb3cn!}t%P IDAT̔7)v۾Ukk;e~Yii*#])ڹM_7jqG@$UW_1]443( 5u-Y(GW] .V/t?8Sa tߦFG9'+-Mw\p]?H &h8`htt#jZojHH~E-L8P826>ب/=pv75ϿH?ir:p#]UWL?5Ur q+spitGpǷ8~}b}jqY81)EgP0<tG$yެlqbApZ{0<}uH\TֿчˮPjr87Fpa>I]GCtoR.^Z>tW )N`im PezEt$ ~}[zm{4uBNuؠ Gt$ VVUkILkzaNʜ5Π8 _)y,)ТFbI*g9~Ape:@pXP,XA$%3?~ʋp`8,t}DF}g~ } ,!Rk(%wLXީjDugz/S $,w* LG1U='BzaN}wj4rrߧ,2ň$~:ڪ:W^ПՆWi:$}b+$ܚ$SNH%0=a۾UoUeA@Ω#` 8T7*ݗjW˿ĉ4-g/> z9/ 7Z_WYiix鲸1.9)If ~=e8IQ;2RRLG)zvSӴB+**bVgeIŗ4;Wxp+rNLt4 HMN _9[gVR@OL8T0 cct6<$GJiIŶBvF̸rf'j @p?an:MGtߦהZQ1 };ؘ83(5~ ^[#eD 34d >JMW88CϨ#@ AK6NsI \ kWim?> $jI|>Ns:UѮGlV׫9%W:]%..UR}aޓ욀BpHfj BuQ4JTMMfd()gI-Mcez6tt~$'W UU\BU "9s M2)9¬,lI-=jk<7#CE]T"U(;-\Pą[ٹt +8B?? iTYX"*,ReA)\Z>m=\o:ʄ3 C 2@djơfd,/_s4h|v/i%y<7kݯ#]UQVq"GG c> 4 7q\jrV*ɦ8L,˒mښ[ĺQoi㏥&'kfARP|yٸ(aMէ\~qxX CIITWt@cc'W3 Uyl=AEA eӕq~lݴt(T&@8$; Tڢ֖yTYŚ]T9ť*g KQ:MG9 ΢8hFnv71_s4OsK4TUY7)I}{]wf\Ae#lhtT;޵pZNKT]\9%_(A +/R³ˌ|A3)Sͽ=jѫJ:z\Z8Lg]0f<6S$ ʎݏ@zm^t$iQY2S2_uv75j`dtYbi|H0>={߽SRea5\*7#Ct~gb8 ZضuMo*?ӯ *gYs4tyX;*W;O{ךBuaHt]A=s޹])Z6R+hqL%1+W Zc2**6ډrZ,pZ#Z_Wk*Ӳ||-.ȀA%9qr=-cfSGpXuI@LՆ~edjyjBd瘎n?"VB ca,fp%0ztf}_ꛏVkj42+^}`eF^ ӯ4#H(QP](0MG?O?W_T}W̙¢nUqI_3Q`9svKܧ}]=6fIQyӣ5Q0d@\Ҭ/a#wѥQ{d @$'%GE{0^>Ƕ͂**VuI)9Q?(v@y!z}_;57޻⢨SCe3MGFw~ o1m?mzT]=(_:]>/k.hj?>;}3'ݶBG`Q0ds!}_omRn Mu 1E# k4҃om_:Lh*,I.ZGQ-Comb\1<&';r3(D|gMx8ڤ{LRϧ΋uKsrUNF28֪=t(1y "~&ΌeexlL?zfwmN"UFgΊpv([Z^퀀ؒ3'&Dr #% DYjr1 T(wٜyF薽g hF]gk:eEΦU&`YL.>:MGqH|IMNn6`? [=]yDP a pАߩtWHIтetK]h 3+i@  ۏ?~Q\YSH! !i>@huj$2uWȚ^\U!(]vQ١{il.y*ϟڦ@ϝ4 AfV*'=t ơZ= 1\gɏfeek^t`(%y<4` vyt WΟ"`ؕ!m_|V!Q\tRse(M\bJw~ c7)I%~e*v0&WΏњơZ1͟|5 ;EpUsl:I/k`dt W肾L_7@׫U̇1wp@t W.)жW_.Xh)j68׫3>hy gCpyZ:t  uc¬3~sR EnXtSp4aܬ¢3~ˢApe(ږzx˛#7#%*.b e.^j:)z^L0,7_ޤS~OCpK| F۾tII*9l]8@" et͂Ec7ժg`t fح?߈ ]x|>1LRh|\/i:QsLsWPfJ.b-^ڻ[ma̴w%+|u0R7-]tFޖ&19qt- Ke% Ģ5LG0SnYB)8&Ep,WFJ&̓M0"SfJr3t=SFpt{g?21)m/OFpb#3d ˥|i 1LҶæ#. Ā,W'h1K}c>&|^>21L-iO1ӢĈK$- mN~aIeW2Hn2D̙k: jT(AQb.յ@ e:pJSFbFc7DA]q^#DA>Wj1t(1j2]V=t gC;QbG.[41A@)Qb?5U\t gI(1yiqYNÖ7g(q/08  dn18  N\hϬ4) P>8aIӫQvz(.D#i5c#I(qٺ꼅c8x>8ˮPiNa xMJ2mKK.7$ı-癎$:v "$c \w&%/IKļYEע$>W_VUĴyiޡ3LGbiu܀[l(@z<.f:pZoEW6)sJJg&{(@XQ1t(e\.1 SrL%c_YLUiNQ0)/YO^q5G qɜ#gopLU-ԗoMi>(X.;t (e{R~t5Ml1[KxHw fPp3{'$̔T]?b/Yw^p,O[D>6A b<;.X}-JD7)I,Xl:0aD;ޯQ||fdL(?~]1o(|^n_qP׫?}byLs͂*s;,b z"};Uc: qi>n[~QU%EKMG" W*+-t `(׫B_=,B\(/еE@-)A6BL[8 1 H\߫_FB!QIa2}զcSFq>t`Bfw!QֿА(i%'%n?`!Q*!jëZ_t>~Ҟ#/tU髯3 \k4~So(aج"}=w1Auui&gޥLE flolxE[MGAKsLG"bKT[^M8s>W_vUD1i4۷okpttġW_V-^f:  bؘٹ]n}K#DoHą۾UOnߢQq})ќQGQWxd[L `23nQEA((K#zfv=kzLA UXVF  kÁzrV0.uѬ*}&?H $}-zz6q6?<wŅdY8@TQp{{䎭zfX0r32kniMG aBtF/٭}-Ml3@VVUcPvZ(1@Rk_^ڻ[߫~q܌ ]RUUUY* lPuBR^]8Jע0GNcxlLoj)1ěghU|]8iP  H^=j5<6f:NzpzVVUYJPH[굽ᰶ7ԫ?h:RBԊYbΛ>CII#1nhlжzi>TCR^͛6]fk2U}`(@BҬGTڬC)&%iNqL/Ӣ*.1 AquvE5ͪikQw?J~_%]T.*|c(@uY:ݥƮNu Q^ff}/.QUQr9& ccjRCWӥ^ ,/_rsUWyJLpP8~u 3P[OzȈlṬ߯)< !|(@ ۶CV#Ra j˶ѣea,4QxM:.ݗ"׫̔Te(3%U; QH@S@($ @($ @($ @($ #)d:1eYSض=b:A)@ض=e:N #t U]IGLcYVǶzA@XU,t=pcYA@XUkٶ Lβ,k033,kܲٶ۲bt8`˲6dtH`0 dIm۞`0))h$N_dY@XKCٶeY)IzPe?^^Sf(} @zz˲VFFF;8q @m?< 8*p7N{*999u!77xN0 :~TJാTJJJl۾':,iSMHҏ$6|/dggή=N7 Is( ۶deem,}og@۶o g,999%=Hieggu'HR8+I#4bWC~Hi?8YRWWWVrrI9peY CCC ܳHR~~~O{4۶?37i@I:i/a`mgee=6;IDATOh @@I ,iIVVVDa#?*-#y&Y$)''Y˲~2AffIMö`0US~~eM'= Iey ڶ}zĨIIIhq6?J|4_`KgU˲Bx_)iz_jmcOEE#zDdff>oY2۶k&e~ x~fvDYOgee=l*@GNB/`d`Xҷ|\wE[CpsrrL\F߿8;|Lm۶ͦȵo}}}Yגn &),a۶0*I,CL ,rrrs&/hmmHOOSҟHD1lI$iӦ 41&33))NIwI:_1lIoIz  =`:d%]g OmhYIIIOGc~'|8mV<۶/RI qI,kж=K(ioYm8ZU8۶,˲ڶ]aYVm-*т|IǞk.) z.I]m[dYV]8TuȲqc)e XԂIENDB`spectral/icons/icon.ico0000644000175000000620000020407613566674120015132 0ustar dilingerstaff (( aa<=!!XX}~}}YY!!==ba̾ŴĴ;}OIIIIIIIIIIIIIIN~ɹKIIIIIIIIIIIIIIIIIIIIIIJȸJIIIIIIIIIIIIIIIIIIIIIIIIIIIIJ˽kIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIdgIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYwIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII`JIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIqIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII̽̾OIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIbIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIILIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII̾IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII̾IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIOIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIoIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII̽wIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIοIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIKfIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII˼IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIͿ̾kIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\lIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIɹRIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII}IIJIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII`ŵIIKȹIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII]|IIIɺIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIpIIIIJIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJIIIIKIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJųIIIIII|IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIn°WIIIIIIOIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIOǶIIIIIIIOIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJjɹIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIKrɹWIIIIIIIII̾IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIm˼lIIIIIIIIII;ĴIIIIIIIIIIIIIIIIIIIIIIIIIIIIlwIIIIIIIIIIIĴIIIIIIIIIIIIIIIIIIIIIIIIJɹuIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIItȸfIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIK˼PIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIMIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIijTIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIi˽dIIIIIIIIIIIIIIIIIIIijIIIIIIIIIIIIIŴcIIIIIIIIIIIIIIIIIIIIIŴ̾IIIIIIIIIIIKɸMIIIIIIIIIIIIIIIIIIIIIII̾IIIIIIIIIIN[IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIKŴNIIIIIIIIIIIIIIIIIIIIIIIIIIIIIOIIIIIIIIʻ|MIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIOIIIIIIIPIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII|IIIIIIdSIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII˼]IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIKIIIIwiIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIKIIIIǶpIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIʺIII`WIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIɹKIIdIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJIIŴJIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIT̽IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIInNIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIjͿIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII˼IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIfIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIοkIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIwIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIInIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIINIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII̽IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIqIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIN˼JIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIInIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIJIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIwIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIfͿIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIi˼LIIIIIIIIIIIIIIIIIIIIIIIIIIIIJͿMIIIIIIIIIIIIIIIIIIIIIIKȸSIIIIIIIIIIIIIIO|ͿƶĴ̾ba>=!!YY~~~ZZ!!>?bb??spectral/screenshots/0002755000175000000620000000000013566674120014724 5ustar dilingerstaffspectral/screenshots/1.png0000644000175000000620000026341213566674120015600 0ustar dilingerstaffPNG  IHDR~sBIT|dtEXtSoftwaregnome-screenshot>-tEXtCreation TimeWed 01 May 2019 11:43:18 AM CST- IDATxw^us65LI !AB $ຫns>>BPt]},>4)ҋ5$L T`&ܟ|Ϗ3f'LzOĶoc&!""""""L[CWDDDDDD0~EDDDDD3"DDDDDDD` """"""REDDDDD&(HMP,""""""5AXDDDDDDj3QD"< N>DƏOZe]x0<*W'""R;lƲm&Ofե겯 ~>ML{GeEDDFb(hR,jZ'xb_sd?#Dxꩧ h]tˎ~\m۶_˿`ܹ?~LWw7'+[VrIڵ~{\t,\zz{{_i400.cե0^K/ s%_mdve_+N~%v}˄USțGX+hhhSNbHOO{1\ X`>m6̙q˺u~ G`mdo O?47o 31}:vf9>cNvIVvRٲe+X=ۻ>y* ^͒%K #8cJMzpL:y# E?3m²^^ """/n:֭[M̷Y0>ƘC,Zo[n~}}$ ygK_{m˷^w"""o/ md.^(A9`/ R0 xo X # Oپݱ?ag^}! t`|,b_VW"""odle^]2v6y2_l]f_كm;̘1c666GnƲ}n͍ڵ'|K˿#ebk+B<ݻٳg/gM̜1o{twwc.\?1|IWs<,R$ ,|byL:=뮿~;MDDr߾GO|rjx G{/W^{-#I'cNnxѽٵ{7_kڵ o~_ߐL&0yG)yǪ|yq-8}i8spcJ[+N;Lrm؎'-[Xv_ 83<]&""2*'CnxXm3}R(""Gx;v]<刈e3il~\.]M7?l_1H-""""""5AXDDDDDDjHM` """"""REDDDDD&(HMP,""""""5asEkoom` """"""REDDDDD&(HMP,""""""5AXDDDDDDj` """"""REDDDDD&8c]~WwZ̾Nji+\T ө?L'EDDDD`7=Kٷ}522RWv ]}@H\/C&\´3gih_EDDDDdԙ0 ñ.BD^-xx۩"WD3!L2}ضm[ ,`a`=pc0e{vDDDDDEdV_u̖BE!iL)'Әw=z AHIJE шMlb{w1wf8ȓΕ}{(,&ѻ?w\kѵltx!!1u6 hΌS_eh[$txaÖ8"D3{h7nTGXDDDD7y_pMؤpBgBB|C- ]%n`rHe3/. "o`7^?w~@c}l1aH 3 U]NS ,M431Xe~{qa)R6)[ fϞͻ;o:ADDDDd(Amۺ'Ic_Gh,J  a8 T=8i2 [ 1q"ˀ1@,˶W_q%K.e671 AbGDP^J8*^dQçN9>(>w)"xY}/Oa_/L+/>F͋WVާːb!e(p=rza@D-X"\x`B!A7 C XX$I.Wh<#?!Ęke[^GW~rj>?G>$129Cr%2.7&?刼)lA$ JA7 0X²mmXHc[WWO&#LbG"Š8X˱ lr !ģIJIui(4bSOqv{yY%CG4d2n;7_=VOcK%' &by<H_Xl|qH&di42if8d,h28C< KIf$si o89JnE ߾kLFDDDD)Хl'LSʓ&:dӬٴmiU: #y~l%ɍ0 8$orolh=!R0e,5'46T Q`UD!{DLc,B |vH R2?}P-MQ]+*6z2HO1x=-;ĸ/3Pߓ_#kֻXqN!mr#u fI<DK C}kk+;8:.Ct 5}<߿}<?G͚AXN*xh8LLh^ \70H_ixfҬhy=Fw'\'jCCcfefV&qLC+I, ^/)vHd|TS+adM}ҁ]1hchS >e%}rtufx R'vs=mS S/,nK~CN6Ge$O{6bۗ1=oK;ģdyؔBTS+bKP IіMR!AdGDDDD5s94>Χߵ)/5t; ٴc%?o9g;whV?8EP u0RvCݵ=^t*SzOXJ?Ffޗn>!=JÄCEf>d<"n/>8jGUwx]I4H͛JЊd$&7ٝ.~@ %;JIP*]rWrm&yF5c5p}o/٫7sw.Zض!}0)朴}Oߜ7i$GBz]8y!Ȣ`ϵkqH7X`c>R?p}I>؄@<~r+2Muh ³ HHjf V}C*0TZZRVI'#A8CGDDDDMaON~z19u>Sĝ(V>_q%qmOrGC9Cuz>K]"Jքc-Wv{d}^HNs ىHmaÞ!!gV%%zz)!N ËCw5R9 CkcI9;BATRT/1K˱sՆCzqW.c5Á7<оe<'=uXwk6#/xLӢ?0 17/S지^oEL'$TuIƇ eaA1нgyώ'&sX`ϥ84D|V T 'drs3h 7`& UòlU1΄ ;S.ɧ >rjOp^HCL'/xUL,Arl -ۼ1UJem,A0doo }mݻ7Kai & Ljj"|vOO@s eq!`ʸFKi4XD 0!P?_l862ؖ_X{jo|V@X }/~ɼyvF-?aD'?}7#߻&P66DW:j8qR!|q>d;J2^c݈m#5e:-y6Lhȑ­"@z>.PP},am٤IT˱@8\'kx&CT>]pKJģQ*Aвx3ƶsDDDDD^_* /pN$FyÕ(2l'ʢO²lʂo0rգꇔLϾD~˥TC/ETUJcBa8[FmЄ` @B|ϧkO7x!Sv].v~}$""""~^tiw[=-3g[t]D8DZC ;S;Obb論h/R,mȾhABR`F℄KwƘG@]8^8v"""""2ްë,nyCsQ2y:U;*xa@qJAQeQ=xT!fvHBaBBw/*04T`ۈ 'bS =C7dgy{`ZL[}K.;{پ=*,c66ml`\ 6?W [8htye㘾rXw,m2"QTXe lfJ[PuX7 tsT+ ^;wC'm9 Uǀp{ϢaMsE;;ALQT>KC˱+)LB>O"*+lݵh"A$'ľoX(d=Ǽ<=c """""#BX FqoQ <<BB087P%g`/[fI4L*m8OP)SBzK{бk/9OΩ;֯/""""2bEr9U:J;|/a>΁â 2~VSbܩLl.}TH"=qRTgW57嫋({ժUƺ"qMȆu='U}? EXTx{;@عo?&Κ9Y`LR rw|"V57; F"""""/d0 Ǻ=xͭ~uٹ0R Dz:Rumr6XE<eZk 4:}:N4I1kQ=ikk;,MyY~L9r.y7-:ǢK% Î; IDATq"4Ē UCJ ~Q#k 38XȨQy4c*fG,?uk`x'x (.e(Fyӱ6ttCqɵL$Z_yhjƧҾc]{Yuu140Dʉx<>g%O6\ƲǬfMMXDƄnX """"""r8(HMP,""""""5AXDDDDDDj` """"""REDDDDD&(HMP,""""""5AXDDDDDDj` """"""REDDDDD&(HMx8(g66<[7nu41cm?Wc6%""""""o)?}{MFpkGpMm^K["""""""oI5!ƶ0#1 Ttmt.ziEzi!`4[DDDDDD^ \(bj3[ԧN]{%T/Ц߻Fbg8zbȾiUظq0GДtzzS8~v)Ts%f,F qcGՑ[x̏Vx=3s~+?| WSҗ?Ƿ?{`_JDDDDDDF<cQ?XVLϒ7`v3a/~Q]-(w<`Hq"Q"v"wryɶ سa[ytWͬLLE;%`y17-oE..Q@ <*E4C3"g~9Ϡ%2TH0Dޏĉ0cUdqqMQ`7vƺ !#ED^=`9ܴs` """"""REDDDDD&(HMp>}RDDDDDDDFFEDDDDD&(HMP,""""""5AXDDDDDDj` """"""REDDDDD&(HMP,""""""59 rD\}hhZ`ECCƍ##HDGY""""""RCF5GP`߾Nv!o_7Jh4.lLss3&H6%`h(""""""5bpH eV24SN#͐'dn˲%3s,f͚M<q@J b1H&4551eQ'b8mT]۶q[ne2yd1QԈAkchl610q NTEDDDDDF4y˞]Cqx~"ˑL& ÐJ!!Q*xG lˢ\*cfΜ5^Vj~?}|dÃM aG<~6~k+:,Nyv1%xNzrfƆ=ppg-kA{^k } Rc,,"H`aH$V]* 7[u]O< z.GT5̹h9EDDDDDF`cf}vdݽx{sGƩZˊ)Z+"""""2F,cH&SؿRL[d&MĮ]!T*,|@Xf77I]]w]tuuDm:RH:ʒ,Yq3He8y|;y<ׯaM#2 ?ZƢ߿waFC_o?=?d( L0l&Y(T*UƏOKKH:jYMXq4^t5N`ֻyLyđ """"""/3KOO9T1vZf77108@C}\b ljr)'Q*)K,]r$ZZ:u GH$ƍ3idHzq"ĝ?C!ca#wLg9oYI9g^CW;8eyuy#r9N㏥X,iraԩS9 ɤLqNQ__OCC=\^U')u+XT>m90 ?诿ڿz68K/:_cp$e\3f1c4l'S?]v3ugLch(am?3th `=I*bI8l,c/4c2ܺlJn,.aqP9ϖ-[={M?a6TSN9L&3HL^E ,r{d2Z[[q;ٱc'F"r%DbKrXvJ&{,#Fds465188=̳<̙a:$HDDDDDDFΨ~D"cǶ#z-^O$g/O<${欳Yg}8JsX->r g̠n}t JX,Ƅ 444DDDDDDCv$--Y(V8R@hl6GKK>Ȩ8yǦP(qRx cƢe KJEnxz{{T*WԌQ^'R̟ٳon2A8<2ݍ8NfsLH&%ONgm{4rX;N{fL}8JmEDDDDD&(HMP,""""""5AXDDDDDDj` """"""REDDDDD&(HMP`0 Ǻ=u""""RC4,""""""5AXDDDDDDj` """"""REDDDDDwQUw$dHU`^w`6,X!"PA%ZHd23G$$Ly?vZ=o)a݌IL-l+c+uzm U<胼rPb7|+?@̠} xMioN^w!Yz|?z0ŤUHg+ye샌Ư"B!B!Hi˪/'li8;բzA r Q џb]<8AMa,jƈE8~ Ë6~Jlׁڌ> 6UT+a6Kv7uM*1"qFٲnvAC@^h0B!BWSO8#^PEShWB Qr /=$#̼Y NW*W6S)P7,&'J#WH~! Y˴>3pg{B2C/~B!⯨V- ?|} z"x}{ٻQ݂㗘::# ) p8z D B[j$P;70J@QXqZ)zM8m;@:{2N? k.d2yXT5#–x*7 _ab .弖2+B!Eifݪ0Ђ:jVxA؃bi)kPGZSKYmAX7uAVǯPd㊰ q=Ea"6> 'UȠu !BшCBIB!' y !B!h$ !B!h$ !B!h$ !B!h$ !B!h$ !B!h$ !B!h$ !B!h$ !B!h$ !B!h$ !B!h$ !B!h$ !B!h$ !B!h$ !B!h$ !B!h$ !B!h'4u0i((X,?(B!BF`]uMurp2bZQUUUOd7B!B4'$A0D4 èU³bjbٰXdƶB!Bک  G3 @ ivl6[+B!ql&@@ P_UV`~0|pM:j !B!z iRRRB0)`&^CeKY%3nszf1\Is^G7tB!a94@Pp{^r=xK +<'6#ڪ>L`E62q aun̬|8kjKOL*3 ƄB!s @Ylڷ=yd &`XiM8:$5]D±Vt0DQGN@OgeYU|m V&򏏺1Ա ;Ym !B!D#P|hc h{rlRvegՍIª4Or;uLSW jBn bma];@+'hBTn^ 3( mƮX'Hb}~w7op`|֦:|#TЁ̚tU#cu0Q6{$Npi1;MK$v_Ȗ!O@0())9ou zoH02ax~dwnQaa4u`TaXO/v>ꉠ밫n[ [JEv |7surTTa)źw)QrQXT g+\~kɴYƹSxy^ ˾fڽٗK&k IDAT1Şpzރ5rDŽLfn%IFbQ:i2zsMq`;fCh҆ۍjn!BF#`F[cgvVL[.ح{hZO\?KY#s9;5 gmMy_ K«sb]2<7v~C~$:V P|XB̂ey d; PO1%-WyG8,r!B!_E*uMjTкaW۵ۮTYN4t]U3mƻ:3Sƫy]_.k?W=;gs6MxP+dwyf.4h\v/7`~!*OFbrs"{_go^[}LgB!h!O.dxIbA>CR3 E!&" zf@"-s?((hqStyxI}Jm04y%{>&y5 , )+P'*_1jd ,mꑇ]ettͤupb!< ##ہD.}=ŋ]œX#ZpynDRZq}99w|Nw|oRQcIٵcy{=+FwetUŮB!B(6=rxnzI`iKJp{z<]&χfU1ܮI"7 JǤfSUPN"=@Qa>p8eǽ_98CG'Y-SW #&:{T5"zpBj$1VSGH( ":0,l,VT0 4T=hHnݺ!B!Z-Ub@_ 黈ghJ*M]-N( vvMTzlI0Oˮ,;^_ PatW͂Z!fT;QDG$ vXQ wŕWTX aսk&*@.-iwwx1U-WP`Z[B!BQo_:jK(kٗY]ѭe+VjٔWjvl8l6#h)]~0EiWpB!BZ\CalڿYYOr;:%5El1W*6e\]ߖSTx87D΍;^fɱuB!BaErp`To:e{(# U\<-bHAÁ<_łF6mqXm|B }^ǡtO:a !B!8j$ni4Q( 9l؛A͎+<&Q4%9 Mpb).pЭe+LM ۉwF7!B!"lX\PݕuMӤD R/2ڈwdvI%jEQl6uĚt\YR=B!B 1NZU(%Za/]Dv3Bx'жIb"!M!B!lZԔ&QD4Cg},߹"늢п]zj=$ !B!NȉQUU*Fy]D$; 5:Vxk󖴊ګB!B`f6*,A:>)jGc%xJ^ +]ya!B!*JV9ت4up~^$DFa;I#};65؞:_%ylںw嵿87oOiS♟SU5ض#݅K,şƭe`ާpxLng~ŲQuhaszEiL6GVICD!B?ZFڶgDSsFz$8F#FeVk7.Vr 354='(*C*PƣYQAL#|-Y&cu ~p-Se2'? `oLHdm7rG?'y(Aݱ\ƲG 6-`K ؛6?_MV}&g&!B!1 !6 M0Cl6]R c58yxEPޭz, 6-W`|{/->﶐zmW Oob#}1khR9fZ|w|>^ٞQL]GWTi↚Bx x~U[Ld!jHN^4T4 Z9*r%YhsDqF)JYKEKs6+gL߇?F{s2 tSA=|Eѡ?q:a9rܱ Ց4tuB!BY:[,v;~=[jLSW K*}'EEhU!\jwd$%f?fY^՟pҭ4X9ui SȤ,FՋwO`ܛ~Oܒ\:ýiSe C!6u8^ӋB`Jf:aݸ9-k{E9,t ]UN2y>tl4?<)<.ui/Ȧ+޸%<<̯'BMMQX<Cy"b6HuמG8 f ?cJ9Fd{&~Ȋv6קcK7\@س}^G{wsiu`)oyWp&nw |m\*Hh/E:8 οAJ1 bhv0NMbz I= @I&?~5 LIOWӾg GB!B:C(aUt:O kl?x{v_0|gErǷS-iC43Zn"ֳ?XgH^8)m xu|ຝS85mI?.;:usVhQw41M MS{=6\<<%AKrqV4fs)6؀ԡ콕OX/s Iy,||/MwAwVqt9Z[$YUz%B{?N@%=;YG+:{k`SbUK'ɦM;Ea ėsѰB!~up8 pdZtbhWڴl8zN_+ׂԎ:fffw3\;#(71}JOGL *0)*(W'"IMٕ^2}62{;mo4G4A2{dXTx4 : YyG H3i烦'H#*%*B!ӫs#!XQjCݎn=ո$Vaa4ԗs;Atf@fu]<9 B6s)<ϥU+m?bڢY6~%@DkmDeVAT8x<~ ~AwV6%Ndq? +N;}lo8uHwq]'Әq ۍ Q8BDTfb-^lN'a Ĥtܕ,Z$=-`8x-<|F<R`Rȯ-`QrQ:-^,#T>tb99 !BmC!8,,nRUbVo#E缁1lc%+G[*X J݊it <{6|(S[Zl`tIR q"vdYMSnmˏw֥+@QUf/H pK dZ"(a/_)kQL $/A32|Ii^7`զ֩q/4:&]˙E%5J?oM䵥 a$iGhyf3@=dUE1߳,X$ _ˎt4P҅V& &=ni<&лeK@ijk)ۉN4M;u[ibMr~_Ɗ< s#[`d'_ß˶JL_nU?C!B?z.f*`mBaXZlFsF3&NxU+CDK_3n+L3SJ3?g]Q~Zs9wقnͣ(qÉi$Ffϒmi[ƍCχݒˮFG_cA1L"ڞƽ-ێ^IKlvL,kE8xw \1ρ 36+Q:WsOHmM]nށ԰/xWr\]Zmiۜ=#xik,V,Ng_͵)aI8Fh-_ه>{XLGˁEkxwϿ xTT#MBD<ZX;FڛD([ +yqDa4TZ O9Ԟ-n2klY6: ѹyצàz꽝( bY}Y8 [mAЭmiW!BpRFh2,B!NB!B#X!B!D X!B!D X!B!D X!B!D X! JKKCQ!NLKKkn &X!B!D`m!z Bgg9=RL4B'##֭[7t7JKK#%%^?C$lٲΝ;7po)B!B! B!B! B!B! B!B! B!B! B!B! B!B! B!B! B!B! B!B! B!B! B!B! B!B!hX?PnrK..4!頉A.7{''a;)B!BЄmdLgW[.@#U68 :5eXǦ lp")B!8!xCOW3unvת%,Iv2WFN[SW=T!B4,yk֬ƍ; =BU)iY;x}V(j,{w^!DȠu !HIIm߾;\׮]ٸqI IDATɧ( [ls !mxKx}V|A=l݁\Ŷ"ܙzoC!B!_W}nO[{+vGuu[Q-<1~|4g9&\G[0sW3yZsCzB!̛7 u%i'5ފ$^*^>w<9;,9Sb2aKm(~MF=X!B!D4|htLX,^7$-<7?lΔTL+~_E<9>+1M\TZMni ɾo_c0{]A_yvli<痷*{v|ν-,.JxG,$^=' x_//|Z ;_5oҪ8 !B8z<˵k$FWV]}kvsQ lFZ,(*gf.nUVB飹B&_GgDtA*,uho#mxY3o ͇2~p,a- XZwm*"hZrѽ%xVɉzv< 3r 1VB? Zn/+--tU]|>:vxr{ڏ?_;o &y1=<շo_.\Y?I[<۷utؑW^y .e[lY5mEվ7|:w\ݻ7gϮfUyܽkvtN;שS'~Jפ;[o ֭[ʪݵ[(Ђ[lY!O߫sY//V"[كqu$]\2oJNƆM<Z@q6ܐ;DQPct]CIlnbQU,V2x )D⊰~7()rSو$L=Tn*jMM7b܀8;{m@B P\\LDDDY,ZWJJ 7oձ\ps̩/+}P@:֯__)դlٲ k׎]=Xrt޾}{ntڕ7Vz4mڴ#fFg(ߵc}gG'Gȋ`e܈NӲG{^/Ku%+BZ"kX4 ǜ (V"\qTכbZU`0Q4TZR,X!\B!N[N'K,aРA_;w.w^ݲe V`0R\j}SrUWHff 'il|y7kTv5?|ï 9I"dÈ- cڄs!BԯzKg}guNRRRˏ;C,Dօ8oҵ{ǎ~ wr\qBr IӄCE B!D ѣ^իEEE5^f-G1nܸzmXt]K.'rj,_SO=Eqqq|-+8B΢MN\E$ԢoB!?'bp۶mDGG{9r 7e˖-8pष[ƍ;f<, SLq*_x<5cժU5nOBmekZY#􌭍`]]grjxm@¹B!SV^(L:4+xO=o|C;vlCwѣGӾ} B{^}9p)G!D 9wKrފ/9ݔSTGsfJ"%7aP05E߄BqB=S߿`0Hqq1v.sÆ c׮]x^ Z U mٵ^[{5R;tP^/#Gp}mu7|Su(+aWwi5h˗/q'Zu/U婧B׏,PSTUe޼y!MѰB^{Pɺ.]ispeg29r=n/8<-KR2kJV&BaȐ!^Z$''3}t4Mnj;q\r)h?:X+ҡل.')CQFnw' s=RͽΖZ-X!cK믏YjaRSSCwq駟cǎ.Q%KT#jӝ?pΝ駟ޠӧ{f-[4iR~+ol) y 'cmMhZ3?XϬJ^_]¤ڭ 88 juB!!]v ޫ Y۶mqEEE^ʪѱ ^{-^!+WqٚH^|vIBY֩)Kҳkќ-tF; V,z:?SS"l!B!{Rݻ~ eJ{YF=.b>䓆F۷ӡCzS<ϭVqXǦ|Nvv{@I KJjy|Emb ش59=bv[ybM9g117JZ]ӗņ ;4!=0aWGKgsqZEr"޴xj\D-.tM!kOTϯZϞ=OH[ !EDD0s̆Fڷoe]ƌ3\פINB+)$0WKՆmj;csߙ?o?/-A^)O^żBցJK;kfoIr6Ĥohm.g#3$cA[>ByѣG%%%ܗN>YfΜI>}nٲe ݅c>}zHLL ⏧sGN-Ywahɵ>8p-q}0$o<>}.\˙5XW4t EEiZx8 VK7M]GX8uLKUՆ(*QGfw:KGZm00K>GuWU=H=Vu4 q/EP (w ̲k\e+vIafzc@Q\C0r牁^, J9l՜ `hf5}U}j3B]r͛7d6nqiڵgƍ ـEwaYY𰗲E6(8pVZ}+CRmʆe62e((gf~^ussϹ!}_>9WoKyBf1R?2nOÆVFK6)ʒ߱ I>/^ ?e{5yeҥ=gN׼Oe''s2cРA(F} 6Yf]p/}wChGyC鯼JPDB` }b4SqGJ$Ğh|<:0s/sE($&Üj(ەNs ;{ Cze0EHX%ۆl/6YU=6ڮŤ0[X+0U{>Lx#ÂTʒ l9+}"5:;Jaf[VStc,]X[0llMwKip??uRغ)r@UDKy¡QǗ߰ޝ7JX};Fp_G#3). ) 'ǥq3 K]$gH4}(IUDtN$D,Lv[VXMTf`EGTffȖ{Db418uBH9PbF:pE/j e߅J^tz-M{'(u"UhF\ 5pY Y8/eTFq.g^J;\(\סHݢҷ=ND)@?eBsDGG( #G_=c1\"-W^9C!΅7(ԁ5>f=[⎔HL$سij݉{%ONա]6o>oy(F'`_š݇m/=aX K&fǒh})^xz|]Snj_y]7R]RJe.>F)Uן[DHydX 0Fz9;}/)Dm7U U%TgTjxv ;*K8l6wNi*874BQP@l/OrJply<\g/erl^my2Ky-Nph@#cUЖLE~nzU͸Dyu#[FǚhN@w !DC3g[o=aci4ݟrV|ll,C@ /*I}\ " >2wBVJ~9OwsM))\qEl}u[ў|e#n‰]fnxN.ҠךڈFii%3N*J dqYhpN];2$敪h.d$*-l'fT;C.5u -nFTX1*ѻ6*h|:sY3G78cwr]H`Mm8{ڵk@DBվ 4Lѕf,BI^Lѕvn_4zSc57S, Sm:6;#[*JQŮ-;ȳ$L5+} `-ϛK iu¦5N{ynĩ,{/((- kE@Rp @w"=8ld~ *U60BLbM-8yvn#I .cm@QZ7[M؟[@ yW }^&PQoc֏ki^kbQǼY}dwA`2ST* vJjGjQTcK%b73b(/[ImU* s6p+BQU9VFήJ`5^Xs IDAToS!o޼y^~|7ddd`6QU JԕGҹ֭XV8=%i7_o%mOrZ)NuVޝ~Aƛ^W_e5i}ɮw'D-V;4)b7^aVGB](:l@LLj<Ի ׍J`ڜC~ :#ݸ׿Ԡ\>Frp: .1N3Mh{2~tӾqiP:qA@Q7*?S^f qd\̔g~FQypX:[4ݻyMA*(ʑp)^ɔg~bEcJ9͎w[  t:ՉABQ[MnRqu6#IϸLBc"ȅ@*{s/f}o#~bC2xM?i:ҾcZ=۷ȋNOO'6zB4NG0?$ K(0sŒ@W'|]H$1Ћ@/Eg6*:\=?(<[VLFW^E^Fwq_jt8z Mlya؊VO4Ưu˳^_/^E|ݳa mYQ kφ*htd?r> WUcppr chqji x4|6|N ]:uPk^\ƮyjiI5G&QGiF$Bq~:uj]}-N~ٵ~VI~ x7d.zkzs+j[2G^  u_K !83ۋ=w38]ݻwo&M]w%s8O'1 j:;qNYGİo:Q!\МVD4%>>e׭[dյU~ !ξk6!B3no>J睮 -a[|9*;x`uɓ] 7p !!IB!j_s,Z絆~\\߿.YiӦ5Yf⸄$B!kQyx"9>WUߵ{lll{z=2o<"##qqqL|u54Fio־P3׸ ܓX!7|E[jzVGyZ>WUU̘1cNggf;n-r-8$B!h@2hPnxJNZw9 =fٳ'',j0aBBsG`!BFg^ggg^zf-..>iN_aL&O>d˯ZYO333믛ܐX! ~c6|sbϚ5 ;vhQ݇jv?X!v;~iC~~ukb \\\N)դI]/oPeoooƍ]A\qnuB!'-CBBUwXXX;~Gϱi0F/Ī.8QNdTxE&,g-wxEӿ{0<8lC[B!Bo{tOOOZ{n{^NF;y˾L ڨ08R 9TƾއpԓǗ<7C{niY2X,L~^O^yE<|l!wGkD՟ O|{ z/b]S~)]ϼynHV&OYAIUՙͪYp|EGkFëYۃ7y}EKx=FQ昽Sq5ZOo z"<>͍okB|rhW^:q@w!Zr`!B/*fffҮ];nɉݻӭ[7Ν;[!v?f탵-[O}ċ,K>'KKMI ГJ/fkK!A`!{VO%h5eo݃-t8mL{smh 6n{  -m޵w"bsv?54}Ά1 Oz[:G y!Qۗy}m.K 6r֧R;)e7A_F,( s\w?_+0u(G_O׿˫K/AEB! iMJqΨzYsO,ғE+شޛ8Ɔ {СS{ KH%!P>V=@DnnX o XٰDۮI?\ֳڃ]Թz?,槴bB0ʋIjj@K^{֖Vu0c]9i 3t؁)BXSP:pq@O9l[\LJ CY.˖ƲC1 >C(ep+%s8؝1<ٶwhˢ_w~-ʗ 1|4\{I"V/Xv[  C~lf$MMX#{h55hSue66WIUQȚ5uz lش0~0Ս4Ѓ|aWԹ?)lI\CY97ߧ!BqV"X--eѳyt^: Ҙ\yl2m*xÿȫ)W_=~MiAV~,C s6!0nenf.HwLoY`q M.$$S3KYƗ:DپU9Vںxxz +J+_5yr#_ʩ3?6Yf1`v/OʤylQ&sC mj XzY" yo^*6/^ΐKY5`픨v[+(%sz4auF6)ǁf2{҇YR9%mK8x[6{0Wlo9\L3ذ@|rd#C5͠6,\FVjfL!Fm#<3Ƕ=[B!" pK8%|23ø7Rs}~"ӟ{y_>p*f,Dۘضe/f@Z:ӿ[2%ؾ>r,G=j˲fw33Ƈ,z?bf(7@7ǜY]>x5Gd{M/>E_Վf/gvo/1XO K5*݊o\Ɯ_wk|~b^'.a jw_}97zO?{Q̙Þ)#hEI4Z-4Vg\͑xNxw 2(TKH][>$y9%דe6k[AtW+,c[On,ffs׽?=\YZZT0t0IF;6 SzPeYuC+hl -Z_-=kMCpl9xvV,n]3Пſ)XDQd𓟳棱$,(4BY~JN0UXQ<;5voG<<=T.mpyUpGO`mk3'!ӓ7%BnZ6% t ]^6*3F^'S t㽇or]K%<3/|*#)&~zV&h*2Y^gذt3 W=nnTs5fRHL$HnWs?~?+ ȶq{4'MUhlf7]Ou絋Q8V^͌ٮ׵uB!⟦u`U^ZBMhPtcIlmsk( v;8$r>>8'$qk~3*^7_M] n>ZFKhl$$:y-bǪ4JeuMeƍ6re‘]h mزl6:>hR6eW72XϮr\qS3)ǎ|7F{"4A 3^7Rv d7[ؒϚ- Fվj4T7?O?'q'Mwl] ѫG>۸ʪ $5I㔶3yw̴-\ N<"ZI;IB!Mv{9TmwHqrF DP*͎FQGmݘ )ajՐpBK^TbaX;gJw`AMMW#yBf]NtB2]KA\/)fX. R~6G_^]yT?}+qӧj>xvEF£Sg|;cjgVjIll^ 1.|\ lÐJ6}1cqZ;NlOx/|IxyFs#調[?F7LA5Ц,";$\#y$ݕvGխ#ڏ'-`$GՃ^Wp;AC;wr=v2Y!BYfRF ڢkTWᨮFj;y[VCX(ZtZMSEAs/[Aw4/ `SSgn}s(xo>Ag/Iɡ \\ t6|P# c)3`Sq p/1`= !<2q0zP^ 7zWUs=TE#RѠi|TV9d=:sݠ8{tfca8P2r\㵙>n8=jnr+jK|>Frq((:-{oDq[ybH}8r^M}2]D6{{~VP4 ^oUDWvI6;pրcEYw\puW:eṮZZzmhLۅK~8k!B!sd,8sf:z\0S,h\]1u@{и* L&o )biHLLdذaz*UѢm(lF{,Svl$J;.Y-ȵGaA#o(Ģc# SI5;Fݱ8?Q6rN>cfY-9Rz;6Unک./W| (;vJKѹմcGSnՎծxÎ]Uj5$θc ՁͮhA2ڡεǿ[cv;[og?^gwzA^^ڵ;BA\\P M&Nll9F 6u8nqUE(F#JE9҄`U IDATJ<a]C]^7-FZToKW6v'XkIEWVtF}ͻAk{?YWG#h_QY=EU8{|2^z?&x=(/ȠG4N2?bl\E[2V|9;yy2^qJx&=g1!.l'YBg'hAV`88l8,fQՁFsBXB#wN奐xLI"Ols\Jٙa 8G^x&I,sE!q`]TEAq؏`^bB探ѠZ,5+E;9 QZp_o$ę&>+_*ΗLRm^{w]=u,B!Bg>v8еihPm:(ۅZk^Yqm8J++QrչtDxBA2W;#@ף k[措:@Ej_T:_I~B!B3O^Պ Ԋ [6oV2;3(+Ś[duX!B! VC%^ +}{)t4zbĴ5RJ5s !ܫ&~ ç: Bf1ׄGTq/{|rpmJ?qh{ 8`n>l5EVux#}nʠ !BqI|T5;~WSi =gX)`ۿ_}se@[~nOtC5{Ӄ ϒKn Hۚ{1sj,L끷w(ϻ~xk^e||]|'GTo,p_xe}S'͖߱+ONb f^[ p%~7\r:`C& 7{xdB?>XAuY539:C!B8I mY 9QÁ4RNnwhϔVCEuر8K sC*e{vշM[Wt$aǡhipx޸0%D $Wz\;^Dfԅ1'yjqdr2*v` B+חǖβ$&v6~9Y4Jvm3_ yahp?GMh[yb ÎUՠktꀊfU!BIkޱջq\])fUXuznM@A,{9Y ;k(î͈|>qJS o1a:ԢUtpCDl_[vPdUvƱaVֺ!-/~{\VZ%qISw'.Cf| fU5;p)I 1un0fՕC0hTsn%vt&澑ITl7a{9gnOSA"FPf$؝U1*]d֣]eD 5a ,RÎJo͇ٴ|vDk<_ъ}=/ԯxze 'ԾiLwͭ1 w~ˡĦԕ;{%kf}[t B{ҵx#{`#s w;0Uǯ-ޛ]{h3nT¶8'J5wSp"SG+Q)Z{sדSeǁ+Cc|/wϫ/bNx;U\3y }=zhB!BZ򼧚ذamHD|E+򬍗(م%^:TV;876V?X4Wt Wؙs PANU [`LyEBaU;e":g{=ʾ?IWOH_c7PQ xnq^7P6 ?';XRy5s]^S&1uY,gcy6qⷯ\ߋk1K{"MA7wznIK %DV^9؊ؒe!sW:y5 ,އ1;mFVC1?p\opc}c7ϲ-#at֜Xt}=;Qe>~2xƌy)T[5{,t<5;}]ݚ~gcZ A Qm'jSi2,5ᰙ. wrw;ՎVs".{XRXj+_n[xsyqT9/Pil-0kG.%B!GGGϗ_~͛X,'?f36l/`޽DFFZݎ4C6h]'N%usM7wax'_\ DwŽA[6M7Zſs2̻I-@En8*qvgQM^A<" 4ر>[|o. aLvغ=30 S"k֣EעQT1 6ύe_v)=11槲HОx$^BQeᴎi,bn:6EP,g}MV5q5h</q)OCI"1H^V!E{ȱfb@e&X낭E^W@`J]X[ 2wod5c{HJ$P¦pIM_0\IHGdg_RoAT8`AtR˂}\h(XI/Ngڣ.$]ԋ.mp악]waQI:!%Gsϣrgb6)& JD$켇e`)`Cp<[̄B!j!qqqs=q溊<== {%::jy+d!/68lͤ(9\A<ܟG'zT "dAd妮}Ԃw9նʽf(#m=JJ+**W:~e1rLϱՔ4n5Trl)+LE[c鵋/Bp-LhUi{!9*ۏYl,/HηSev![o"'erp<&h,sL3XJRns#s;fN~eI9C%fJՇQf=28}PGzgֽ[s( FF*^t FW BIgX׼'_oTLp w+omao![.$?B!B'Z%j5ɉ(Zb^Vn*ۇy{k?_et=*IGhq61F [*(p:EںmqP#-,Xƈju8;MF\>F 3N_BIBU 'b]uPj\q Jfr kOY^N`JȠ(KYӊgD;NЭ(u,]FNWzSAh #}QT Jtx&@1`4 3O8FdqhKJg_*G9=#Q(J͵EkeV8n-_1p2O eʟGύ˧ib/!^_!B/qڕڪpZ{hnFUj( q[M&;ZENz>DtZ ֪gǏ?ϭu 6FwHIъ twHww λcA?(pO_}}wYxc[!jF9(WlN<68jly %{b(C6A>!\My= BXXEn#FQ zrv!bCP,{ jiO.8Ht.ed (u_2#^$;@s!殿@c9Z,Ag9^Zϲ Cq;ȎT$\BܝaB T;9{W,7S`9jz`1%!q]xKvNJMrTTKoay$Ӓ$I$Im܉(M)@CbK$ָ;DGk'V(BKm@XQܷ^\XH%9Ovx!W}~8Vt(…f kqߚW2aT4!ܛCY)Q벅TʝUXprH[7#р;֣<2)c?ABME܋Ԧp<8$q(]HǞR󻳇ap"B[WsU*k/>5"?Խhޑ%4:^w7 *A=OrAyoi(b{z'-bθׂIv]\%0:!սCK>A׻|S^0Dz\ųpQs $| 3&}ς)m%%E=AL ; Vk"cTk;g5znuuH>:]$I$IS!@sB;pjtYUaw 5 rg[Ձ:TBf^dT?PTO<ǩb1yB޹uhCEףSr˱dC88Y`31.xwmN>o*fW{MXuKT6 N;w/r{{ݹ?ENݩ_@hϻEdٴxzr>‰=^>ӚM==q^ɬi>e`3VWbs<&4~'#Y+~:Յl"iSqjy8a,M}{㈝vlTբE|š 66… ?q$I$Ig= $ݺιSXt*&Snpy֬N_SL%I$IsZ bd-x?oc J@1$I$Il$_![%3l?HJ c)$̶)ToKrxI {J$I)i5}w?98u6'F Ҋ'R-d$«\ރ52tG棛7a:4=xj9iIOn#z|{^쳈[_ -Zv݇8oo3ckiSd&'1xK?YX$INѓqevՄc[N$qe(s71U[F4=5ԫl]-e˕CM: ;(-SV~_ &I,9޷Ա=5k jFϩ1I=(ӮE?/$2'=ĕU,:Wvm^cw %Qz32=:fE 2c;/X.U@YR#@ 6g/F[޻m{*f5R > 4$m{)BBήś9MXxJcdfg"s^w1<$e{S>,=kf1}Z^(U?ãڧ[|^e{*^c .MU_6͟Μ8IR!b;Mޛt-%oz&'g*ta7Yp'T( dpb<֞ZOHԬl?9+AP<^Z>n%NӘ)tpүbٿq GPsXyNOxD0TnÊޔ+ by"yp 1u5F]zˬ+~թC̆L+d]<b%gl)R=R*# VquXbЗ n@NNʭ2C91Rv]U)X8yL8Rr 9uBb~$͒}g9uʈw*I8eCpf[㦁˘ZVO)x-srIn=$_wmǖcMc˘x3gQʓms<7j(\>wŰ%qiL[S ˏ{^odь9,| [*ʭ=>{)'RNL9L]+Q;'T394~TKrŝ2;YDqk7OܙSG2{.X>u `\\+e s}H',I$I+JV-bA=ah_Ӿ\uQ8R'`ij=5y3y[~$WokAIƂ7[ģ<\H\;6m= P_8o?+*C7dx6A'dbX۶|-/d}Ҟ.Ob/0UQIO"dG/3 .cڼ<]-@BXݖ:۹,A*wvM\;ͣ{NP0u:N:V219HjAQ_}Ú dG/gچk؂*QYlO&; /WYr;2怍N~Οm!ouF})hn}u ־Kc9":ǘ:q-Ɏ)~&9 M>K .oO]i1`1\}.>e{:huDF ľfft"0y5ޓilKkpdrtraLI%&pd:FszS&}ΐ1l3no PcV>̈ǯ﷣SX2IN7ԘɈOh8o,O|FK +td$e1 p7=I۾>?qfŘBli$gQoO#dl\+,څz֌8]r81m:/nqU} ti _{+PrnrO^~bf,5o3ha===Y7oxG:[-EGꖱ~:%IE (N9%?WHX~1#'.IsLH$ nܸo I[fSfq{YoBYzz1v1U\%*,.;Pc.Zq,#Wo$٢sbp^*"˷E,v":ͼ&ҷ'jU-VsYNqʋߟ6)m EE$oQBU\)*FNWUVT..sOcQ`%k*Ԩ%>aBiQ kB^ Dg6bbߢޢjQY!L׍E)bi*DŨoy]Qㅏ.qNТ .wcJѷR]l"zl3Qr 1SubUDDŽwncb7 gl"j|_lzhǤOb̼bŘƕk0U}ߩdmxaa_"^VcsD k IDATϦDǗ:_.ڄ|ѣZ'1PPD*y[ϊm n şDrmń"~,;wߴb`}]ީxinQu"A!n]+'#t;D֢B"Ɩw6#T5 kq؜-v֓E ۮoA7qё}DF#1^xWlJ8Q)%fŪBxʢz"Q,]U4}\֐EP7#NZ;HT=T1 mQbO_EDV(_s DŨY"Xʼn7\+իbjC!Ĝ+nboŒJMZ)Vn >b"nx| 1pCj^bːOv 0}ڃ7taGG$PȻ K_6K;Qb-WQǭԆ$=/dI$IUZX9Eh}_z&߉QH:eqNJH.%J,YgDON#co"& W7ɶE txz=~>^_fK1,.&sUIN_Lz;c3Xɲ9xz/1YQ9s缱!wHUȰ$+ Ʊh_ݵ ظ/8unxtاSr?Z5HZP\}Fy & 86i;usIeG`T?rߩP\<鉋5l;Iѷ)6ms[?teYyzya]8ՔrYq#6kݼćAZm^H=^ p'^lLv W/2{N.kmhҞrsҡv5oMQL./uQ4ۈ) ӯ7sD/ɶZI:_"ެwfssH#k)VKijRwI $I$tEV͏G6%ƍoCj˞ bL6Nbn^?ަ {?pJ:g5Zq:T>w_VOX]ּi -*sF^H?(ϊ|TiҜ2;6w8z%dΑ]l&Ry`-(z| ߠCYqF{Ӆ>:-s:*z\bBaLz $Ǯr?X6(Z6 32&|< JtSRX:5ZMxܴ@%Q=BHߙ֝*94hHʻܟt9uevT-J/heڰ8>U_b1Bt>t^cH-E/-I .=&_)nEǼbe-Zծ˹l-L:ݜ[XuJڥ0b4^%PK:5yf=ьty5‹>739B8p nh©P)IЇ#W3~#됴ykS8j›m^0zJVn x9M;u[n@p8J5I2o`!ήM\f*.+E|~Mos'rT+UwqEۥ`Ŕ3@ہWI`2b^ lؒ`I$ITNh#QNآu2^P9(3]7ލ]R28ΏK»0ruR"kq7,'0p o/Wqy8Yޘ{C駈ڕR'*]$ 6ddҼyḏIxE2x`9[0[/ݎNa|UF C@%j5πwS%m;OiI @+'0Ғ޵Y9;sew{RR;^ M~ct츋ȵ zg !U柳 x/]dSd:b=Eԧ_rD}ٕWFQ76oGg.cOm"d$+@M/ѿ̣]5ţxrՅ-)a*^"+Tj3?pW8c19vjFWޠu w*R>8[ ƪfֆ ֥umү^!ͷuנr m6;4E"*oT-6DMoڦhnw-!Q7 q oHj!SƌL\EPxPPPkIz3!=H|fU?`%JogB&sLK-$I5tTuM-f&78o\ Iz`I$I$IR\h,vC $I$I$I $I$I$I\ $I$I$I\ $I$I$I\ $I$I$I\ $I$I$I\ $I$I$I\ $I$I$I\8UAZVF1'زN+N!h sAcC1|Ѹ'I$I$I$= d6qv6rHHLR E+@Shp8(5 ih55k$I$I$I[``I\-K$h-="(ZWlpʼnqU,x(v 8$nhCP G$I$I$Is'Nk s YlݝBD8q*H\ =]QZpV+=nz\s(h;Zs":{&Z4uȒ$I$I$Is%3%$-CC:d8 OwRJ%Ftp8 V?6D$ƥN@=;*Zg! ]$I$I$Iz<8lk7a 4p9phIu+VrၢhPt-VEhpswC5ǎ: n;ɎM]l8rbA.u[v gGKpŲ7l݈=!%C I$I$I_N6; Z=Jb ?~ CPTT!ZhyVjwѻp|@Bmʅqժ8 @[L\w]!JCj՚g!fZ{Uzx]4_ 89X@O]KC$I$I$Izv-bSA㛟+ɤ'[p8 h][mhZ4@Ntt`KQܵ]ȴd5xI(7oh@8-ɠuE38~?yk kcv5VRD{h7b&kyZn8&]qd$I$I$IiN]$"I2{ 'ljS ТՎont5ǎ#hqJA" rh|J!Cтmp+L*.npz~K6FsOF-i_%{J楽,z: Um@l&zܾB*iћ\/% ]ChѫU7 pfk#J&hQ*ʊH"J Ѷj~|r=kb TFMlgܦ)޴gp)P^I$I$Ilb6r:&& N57M3IQ{] 2Vob 1cvǒx+%?{?٠ ^zW<"_mϨ:%I$I$IazB ѷZ6/@#f`3[MBfǠZ)+8/FPÁiSq8zfwEk1Ui@c|(Ӊ9|<7gc@48#^8W%.:gDZ{)ھZmԻݗPP~j\šƙ9,f~4؈7 K.fl\^:qS|Rޅuz+LռPPI-?ψ:h%Ɍ탂Ujuú X/̡C_Y~4U-I$I$IS-&t~EˁlT)݂p8qӘ*86fLY9:@ qحbLw n.*8v4(hP) NU7?+N0=ۚy@'#6!Cu8p8`zeKќL~|FME;Xߓ/{SmWnOFUO>OqX/n 5BbwF*(ٺ;/b+XD;7]+KѫNz8*Opxj5,ÝTץ@ {pv&7g$I|K={/o8obO$zBf[Xmakt"+g̍C[z2HҿOɦLg;+l& t!\yxS@N)ΠAEv|T/fڋS&0%Y:ЪfNtZ-v'zFEQK.Ndz$'ec[M},TA [_ijWTkؚ7_iE-Ϗ:ig >oV=%Logv X>RqK^.m0%$jI $I"I룛¯LeޛՅ9du;^%}|k=9IK@@޲vc x$vP?ɲj??N90C~e21,| g\[X//FZiӒPS*v}]bW'V ?O.3gwA?R?gwf(MJs椑j6^~>*؍i(I`'IAp'ˮGᥥ*HVhq.5NuP׍b%G1B1 8Ԩ-& gUUE jN AIAQ󾬇C/FPfXz?5%-0qێ|lu5jte3|eɺ{tRg/JN.EY`,ś5ImƏh637 ^D? 0Mh5 n-.:ׂ*`GѺms#]7=8thl&yx# _H.D %|d yIj5ncչףv on"ۭڌA;~uxSd>B{8Nt:-+%:Oo[uzX6LOl=e:JU2m^ڍl@>ϢT-Ʀ:)wNr NOyR>܏8gK;αJI5IWJU$s-Niw*T.w/e+Vd2"hک# >>ò9E]B>eˑ$ТdNrj E%T a\UA,\FxlWpQRѓSoեK8VlO`;.PI?ū+‹:Ӹ' NbweCRq^aΫEѭ[}.fʓdUUT 5QE{ k,rdSKb< IDATW̶cxWzWU/P.yRr!I^,Ës;5 P/}h+Tq–%z RvQ-(w2b\2Rug^rh+.QtEPR"cK!V[Q]iX K$M3a",Yf*F\X3'PYIj*0gU  #lXdͤqZlhN!P@Ss`]@jhH> ́ƎAS%#=͉lFtU1ɞ eZwlE%']E(R <}sX28w?L>/иyc0qUӍׁ>5jerb 3(hѭѳ~gȸuq#s*%hR/jwnK|2fgӲL>̑?Q>'/I$9CMߚ.\[8=wG#`t`-IH\;ͣ{NP0u:N:V219H1Sz F@X2INƩ8e: $a-#VD'uTzM8p^/#&3L)OZ&?Х'Kr'SL~eC}Cd!Nr+s;_EF2sCwYWNFtlW{'Nn9j#q cYRSȲ Tsi98 1uޜ@֧=C6$?z;:i$_~q)YޖCVpKdz5ɴ[S=Ư3C1LvYa$j$o7^I׾ee|՗9"#lFRR$;5L+r%&2M -Ǒ[qK:>kӚc2~a8#]A5dstJv$$n+=q@ɘ3I79LRӁ)|6Zm6~Ս>c"i؃N|bY2 _aI?~wqf2"CZqNnwc'/cn>+xX0G3z;[f6%W}A׺ѳ8 䜘ANCXPɎ^δ װC+gxg;m! KV I=!$X8yAT!=FjkYLsP(9IgqpWh( @/(6z8h" Z2ӈH@h~܃`%'ˌ !ʝNbBA<浀&'ZhLv]h0xPLzg( auդk| .uE4h4Co5iы~ҙC{JĜ~ @xލywďi6U]6L.)#/4(Z#oGEغ$I۫?;mCm }V5;-۷E{7)qдG?w0Rh6cz9_m!ksr_efT( 6Hױ3Vo0:R}4+! $KafnAPz(Q/P*$>O@M A(Uc9v+^ޢln=o =*.`mJ.;d-CBfMM\6/=\ٵ䪃5ɡnȇ>ub -7Xq8?zJWz.v9V$Ů7HB Otq\՘ٜ="B*? aG봂"oW-gVNH\< j"hbwa8QNAǝZUƮYPޞj$ˡv oǏ _f)Loo||N6by{7 Ѽ5/r7)lk Ęi≏UC`ט'9YYh'_K$y$LՋ,&Xp9Txj Uew 3dxǫ &Wpg"Ot_[G4`?N. *N D ?+dٗcڮDI#ҟ9ti>A)p?brQSLpx%[&p"f-3I8[M,mω=6vP}$y4_pן7c`M F2'-ʮXܸ[.U};:ؓsX]ʬEP1 }w1wy3,l_VEuh۶1<\xP|&lixC$f~])gmFynj0QLIJ1WӋGDF8MubDaZƏS߂Vdu6/ys-j|sul{x[2>wleжt=˼u 4}0K 7mY+'{>N:( !hAg}I!q UDI2#˞` o%Y!ݖ7=UvoLz a4<ѫl%1Ss٢, PUb(aM.4㪠`XAPPCM#km}nwxS>XQ(SmxbkT,6l!`zCbO ʮZ|y2 )mCDҕw-5^ə[;[F)-41 Tm(m?Ok~s/oǗL!m·xSG+tʟ(m/ t<o m}z訓9>K@ESHnz}Ū^"Lꘪe((NP6Ղ$bbٹ"sCLdt9w|#;TŘ+bŲ.njXwPޡWW<}?)>5O78T(fbǯzQ*Uuͣ}).S=ѹ=NA[#cH++()D4P\ԉ-'6I=BҴbj+ibM!į"?`+̟>dti~l5w=']oWo-eɔf84o|؃?q:fݏmC[>Nl5;2":I}[[zJO;zkςd#ֲ:\ѣ=z4gؓ~itJ1e9uvdضN:GRSBw Ҹs3Ov_bnbLQ l]'+jcm0{R]zPޑ̝1sp-{ɡ]Ү2ns5HbLTP~HAɤSy*7fõ}w"GWݥ3ig7og#[WdS[wӗml|Xd/[C1yo6qTz9w-<|ͦCP+=lfİ'tnc!աh xEŊjBNNwe:6`M$SR0m)\n6`)(DsPQQTdꏵU64džϫE{4)#I$S_cIqkBQ&s<r«S]r~fгon W՜À6[ե\l٤Կ ^ɀ%;5N&\E,7a=qt՜׷}[S^dvl|;/#0{og׫7Oq7VǾwD_qBNѡ\rQe #x6rpuhJ^^vႛN.uqJIYodžcջ^cn:7s6ǐ?k?īرe5b]z*en Cޟ4g~h=]{PrD<n|U_2mn a7IR~>7x민ǐC^ c_9L7KcL\Nj&OulH;qW9w]U 95YŠ\H7,hq)YK?83cK3uL:+cxt*~WrQpXVeLZԝ۪rMy˹p]ʔI_S4n"'>qϠ_L{ijlg`aq_|c^)޲8i{9`:"55p (5*E*xq׮";ˇiDjT=nV+a`,ffiOdX㲢(`XH& ̬CQBHIu !7amx S8-FcF2$Ɗ+#.grYt|̋tғ9 EXHeq帔~>|籢I e6^*J<Y^eۘL'>E#0Fz"69{ssqhHV&E+h}J^86;UxGjqѦk9m<F(KNdZt8~'cU۝-Ũh;QCJHr:#[hm <:uJ[}ؓ, aҫp$F} {cJ2PCؓEqx@#pF~"6{r~(8ޣ=n R[ڎNݺZ[e=(y۩eZVT)ۃ ߅ޝ2рZl%tՁn6?)[2rSS`gu[B| 'LND8ڔѳ[{vl?LU][^LʊIr{C6@jWO6\_4f=4X#UY#Wb/6&DCa.Mjhu+ӧo q9,TcXmxSlAifVTw$>JYI.&iP]$JhFnGl!~k֬!??; !D32hQdA~|t ۃge 2Lׅ!P@Yd:-m*a (sm^:z]2y 3]:TUTUT5bJ6i:8:sPÈ0:7ZUOjJ&vBJ& !;ՙJ?7q3N5LZ%OD?w*'Y !ap~W8 ƥ,N;`ٰDQ1*Rs QIBaבruzD t,[xS&g nT]WEPLχբp>=L!%c57ldKNFL܍x! BD4$kPUP ,Hx NRbYQT4F`Ճ(j#P,mKIOwhCfF  <*rsF14QYѭ8jC[ !BfTyT+ !UhvPbadD4A]4ɦ(U),, }J2'Vӌ`$L[ *f&PŢsS m:`j8L;>]5(Uu󥠪PS]×PB!BUEIօR 8YSjS`}5)Yj%'8[Yщj뱓4#~Fx f~:5뫨4ݙttO Q :0Mn A,bpz\wkX[@U qqE`P(n~Z'}16YSH+`*d?:hɸry{e:'N&yL~m>G7񦺰*`Zs- w(L&(D !B!Κx-UXn`s0dd<%M|ڽٔec :pB$a2uweg6ӇauУX,c:a*`Iebwl瓧 y`H 躉I*VMپ/svA%.{_cocoBu\`:ITjzbڴ>S'iX4x.~Y$B!xLۛ5k7cX-vTEDA:QU;z҇A5t- +˅ۮ i$q"4Z` Os{q]Ew^xĈlb( IDAT8&} ]wy~V]F`'bexOy>'{KNB!B_N{̠Baf}͉``&b̙WMnV. H氁*&I3ɪkJscSLdP08z"N<rcN qW}Nː"IA [c'ajI1xXm*YA~Qz`}iP3}d1pVυNv<:P01􆟤iXCAk쨚D ^'"TH***qU#rJkv(|>i(@Q\|מzCc#Z>nFX#' V4!;5;?b#=esѾS҆ !B!įA8d+ЂsP$iGFW~u(4ebaU1M 7a4MEÓOF45xNO4AQ "u:t>-xgһ<ز;) P$X[&nϞVN:4kq% &~Cl [B!VBy`KN@'@` J>F$# ň XX ѮS{RcˡW%fU3܏^O$,B!0*#Mx05SKs jIljAHL]r"IGPWS1t7 USft +HoB!B_>*Wf*,de ӝ 15Q1Vl8u:C PPڅCĝIłm9sDH6n 2xXl2aQǁuO#FX,V VBqh/d_P1"M'iaoSo8HC !BZ7[؁>n0*Wuz_KզDC$"X\6#Mm X|wP-VVju-86 A͜yvkCSx= |Nv:3ٺTzt3q8;:3?"hA9W7&CZlx1?CXӆ9"z|!ͫ]؛#a!D72)|_鱌(Jn !B!!8VɂWcW#O")ȷOe/'ȞsՓy⇘xuzn=#Ix3I f\/ڌH]=3p ё+RJe9&t5VSS:s^^sOS Jp>/u*G>9ۊ76sȓ2S_dO\ɛϜKWS+]u/#o\Zg8=/B!p[=QKQc읯9~?ni JU-ßxO{,㵉9Ln 4iF\/:9cdڝ{'OJ̮'#۽ZwQzu"oMEb^,"@l; |^_/U ?s q`R grϹهxO52sU2CSRĹwnx}Kj01;- Շ-9Z%3ZpXUD}{ !B!/N@Gkӂ)Ye#5s]GQ,b YcɥC~?ssOwpe3p<ԍ.o6u4;AR˶(ƹ\d5f}gqWJRl!V.XzB:>;O1{c@ K全 +t6#,哵umg#?zr@du,o}:j7-f#>>G>aaUU/Ʉ$B!hX%Af#(G~y9 "ưv広,e| J01tS%x}lHXw+ Z_O&i94 zb m iUx0@]҆bCd@ #ա|AR7Q51i ֤͂B3>͛aff!BHGPUAjb6w>;)q.EN!s9USyRvP,.3\MkreeHZHOQ>B!B_g_p B!JB!BqXhvE;ZfK B!-PUhv B!BY`PU&B!؞PU&B! 2mB!BjvH3lfӈc_3;hB!BIj\}9jĖ_ &Ƀ(+/8rB!BojQ`wV_W`3 9luŝշG&^Wg>2s2 -kA԰d\~!7ZCs =[~Dǿq5K]ĊҚpo akXt~}D6-gAř޳B-.C!B_f8+BͲ[9-;GzE )VL!k vLgq@^(5'Pj>yĹԧ猱yS jPs9-z'QKXϧϢl=@Y{TnLt5o)^N)@p)扷!f0 j눠A͂<E y霘cߝ߿L\u߽eq=GjmE68_߳2cb\?k\}t.V^K )On6 lm{pEC蕲Y-`qG6.89NV/V%{Br߸p/UaӖ#iXb+xw: _zx/ɥXֱdwҋOo[ 8\.ΚýOLg}2Sܺ:WNqe"Wז㓓Y]Pֹ5p㚋NmIoβ IS!8;/Ly%)>{NkzZO^'Vjc;b[673Kw|E.dCDڦ'ba |9v@gylX8a +r'R}×f5nR!u6/SJ`*.J (q,/%Ogfs/v36F9r9U>O~Idhk]ɫQo>?z!BEoR0[-͖Vt6)#i_kq%OǵUL8]LƣV ב:q8!B!wڿۂ`vCLI+:Ԣ@ᴒ} wcf Ukg#TSgLw[#$aUr׼<țoapsx_y(d,MnNЉ(7`G,x6CD!H$N?`КSqYZ?qZv 0۸'ϣn@; W$| ݸ5g؃gᰐv(0#]GŚtqcOIÙ5J<& !6ׄI !BwS8<[JZ+n%(pбs{_[3)ƷS9N烕-aUKi IDATY|Ì- L3ET-wib $cͼ{mM׬?Uk!K ʙLYD$X9kt钇&ݝ`*b$jV3ocd(b_-I Y vo/mhX4H8NC)X,*z4F|>I@flӾDPB#)+cŗSHyai 7fUDY \=+(7k=mz4U5,j`8aUn)]tҞ&@vqQݡ'7ޢt7EM% wI.T/w>ZUV6!Y׭amW[!BZ=kJi]_1?"tPY]m杂ɭ:}]rxٿ2UV:j2yO,(gr\GܥGeLRҾ/*S=MӮvUQhhN.jUCZ6C.:fQГR<8VpםZ#/?4E4픞01e +s4 ŒIiu-S˟DїQvsdѭԷgtGrdƭ7Ti'K(sY夠rXaʸɍʿ<<)—?> 0k,%== `vڤݲ$vS'ZRo:Iql卿ˏ[zT<^'2u" Łmg[dfnֿyxQ)]Н4L6L{mg6NSܸ, wU_眻[SW[ֺGuN=P@l qFY6Gr|Ϲi}wlLa1ŽϻÉ(  _r6\7Uvr/[*A՗r@I꿷}>UeC|BowX1=<Ӷ</oL/IY4<=*ܗWiœV^}N7yLu\Bo{~o>۽&:}ػ}3LFeHIi8~4p.ˇs ;q鰿dV?4 "{.fy+gN?[l[DDDDD Z-""""""s,""""""4 """"""((H,""""""4 """"""((H,""""""4 """"""((H,""""""4 """"""((H,""""""4 """"""((H,""""""4 qQʿ[X&3""""""(\ +\.w$`eA o= """"""0Yv\9PssfUMvKaح xk2ѤKɠy <&J""""""RnC7.L1ƪ[wA{L4\3Zӳ{;n;ʶ\O^9i`;"|5J t> KhEDDDDD;..ټ2y6%awBo;$ 78M-F.[/Y6Ipm). cZmG }_xp^M;vQ9ۜc[Իt#<4"E xeX) t#"OA*.wgTʶ. rA-mӅ%tQIu={SG,Ǥŝ@t4n^n=:Vu~(.YٞЛGAӤF7L.k[9$EmXl.q56<0w-<T3؁=wf>ǴrMV6/ɳY% 3nI9=w+EJ>Yo{ֱT:P[t.?&Q? &'YMY5IʳC w_ow"{4%擉K(nѝ dq/ زO'dAv ms3g;s?eb{{6i+c4+jư1==wC2\~~Y6c!I~^!]q(p8\=0w͋o0ʖcӉ.eKBK>Kc1 -{ C 7Ɵ1}l!{W.`̭Gq{6<Γج|p\+Y qyJ#Fapo|v1 d2ចğ7>1n4/^S.jٷMׯ;T}.EϻgF-"""""W:E[J"/eeTVmգK". |fxvݰnm7fKNōß^y'RNQߜIfaG5vd+;r*W~b=E?-V,F Hwz:ӹDz>R64k@lq֖B%yg*Vش,,%6L%RG͙ͿywW0\"e12׭=_:n:߮s(-2 z5ԢUIzAiq\jڏ.bo3zfv=ݫ@|)|?[_KA/fBn=`QZ5=x L)<ȅt13n&4O~5mX4iӂIJMɍAڞ7[05d]˥9='o.w?͇]_DxEDDDDsl,8n>ϱ՟j$SZeB^>J<\9$z?@ɾVHvA1:XDDDDDg°Kv+I~N&YCDcPKaa}aFW;.d: /`=/szqX..nΦޤ9[6w_z&cTv9)A1/OON&lΰKoO6 L/}b{v[QDq0&?Ƚu5{0=x*$8z &:e\+n㲣>lH45L\8%_aSO7o:?jp3b93Lv75N^Z' ^AͻwLdr |]Sm866&xD)-.%H"%`qndӜ ygPXTLp SXanױq0'Jiq1!HIǷa.Vb?Ը}wM61_!\RLJ %sH^#++mnH#R @q%bՆ_ҿ)on  d,*.Dml: 0?KBp;4M O]o|2;;^Rҫ޳]ن`J*ݰT{?1EDDDDD~)48T} 7.PV.s"k޵XeDDDDDDD_y`A[&n@m.!i93 t&t<c:SR3 uzA`9:`0Hqj{H>ۮ'go%y|9{J )!1ŬJ\R_%\f׳Y3%R W|KWm/ j"ZYI:`iHGA!8,*Lե8KTcTrs\yu|1g<|\;ZF~[v6ϼ>e uuM??-"Cu]T2o'0ƿW"""""Nd͓|!̖2E&n  \/=f»Kp8ADo?rYa].\άs\ڶiT{E&xv.:ct,.ni6߭Leiv2wHq&t^ߞXx~):w,:t`Bm\bm8Uߕ7T86.hFrY,CX՜#"""""K0ek؊R]! ,.c;k2]ŵcD]/ ޝW"@<~3(̟V5bppvx<\=YxEQH1`K!Т?W^>~I!OygggSjCba\w`'?cPo80l>zr_m(ÍoŨq2k̉]n>ٓۥVQ8\nݎ Z5^!&]VpȖUB˦^5Aس^e)t2f*CI): ,ZȿOH/+gS3iTn:7, ǬED0Hh4WAo3 vk+Rę9&Q^6X]؋̣Zۓ}l$5ΔI+Kf\֣S8i_0ЃmMV~_Y׎+Ǐ;c-+K oR&}Cгi=e #ۇ̥\,zso/g)hi&--2iFǭcw- zf1%.㮳:.EEE$''nH#R* b%y9K"sMjɤˡsͯG9K"q f_~ ls=p݄΍\H9W ]GftZ6W6=3?펤w oZ<4r'rh1(z"2oKJW&ܱ `N="x2ݻ_OA{Ihޥ <%l+*:p4 ֩8أs 4N޻=6wt;RwJGO1[ wΒN.4 Hr J$kz 3cQ9F&Z3z@/bvi4`9x 9l y*e@isK cU%-5c)rHJL'ó-1fTl×>dzBb4;9ZJDDDDD`vlgX'!f/cCvvetbb`෠e\3Cy^^6|/՗aމgl>*BD8(q{x8#\ͭCS:4  Q|  IDATղ).A  H'LQiPBc{`mqf;ԄsۗDDDDDQ ޛ]Fc6寃w1i}ba1.)>v2=kF${/y LIl\>^O6efI4ON{6DMa\l- ^iƀ#X\9KY `x,XPmZԹٔپ[ @n} VѢk;qt7_ N1 Ov#Q}߰,,;L(bGT.v/ɡܭ3\e;Q)b}YDDDDDC*1 ˀxEۦ)4Iq6ʶ(yeQcG\*bvHArq$yx,A(i{®\xnIuoC58BNkiU[5 sGxX^<錸?Av]bCwm^@vW sZ ׅn;}ߔ6_ըg`z{ܟayy޸DB);g;ay?qAoXNdߊ z[9}\5|C``^}$}ј= &cM ^߇/9Z^DDDDD~vwWמ//JҺrqt,i $lHU-""""?C ύ4 """"""((H,""""""4 """"""((H,""""""4 """"""((H,""""""4 """"""((H,""""""4 """"""((H,""""""4 "˺Q6/$Fβey.lbaТ=P/x螈44&S#dNvvtEl `خ:}n#}t=Fp]mFsŘz ^W[i,3p\=h۶4" y$vkeo#~ǐ+]9<EDDD`%բ31cB9/*$ƻחfInDDDDDD$Emv2=:$wh˴BL[ECpm˿K9͋aS3y!S#s-?4!̤ qm ۶V3w9ϖ3P[739]S?_Ai.""""""u]wUדywpv}f&m76Ií|iOFއmeGo&+FZ|3aن~rNkUgZX+aMON|:vX;cyc-hGTUw>.÷߰hIv-I8h8%fh_y˜&&;pY;{"/)YE[&4@@QQɇ""""҈Թ,ƌ͂W5-wx99 Vt<$N}{GĂ2|_q&<3*it7VA/o/cKUF΅W4VGurMW;ŋ>\v|;Y& w=zkp%;k}{_@/9×⌴<)_:U}e?Ef7zPȜQ$N8fT|B[,n-9%lޮ-yl)::Xb1bv!-{ n>o*3p%Gt7d$7^3԰nxXe^ !GY_9%}P S#o~Xɝ,< 8뱠sF0sY!z\ډ,~tC O]%)a -IaB6UPAN8ݟ;QB JˠMJկ Es‹٘V,FIr#9 6""""":GB.SI-_@$ z5I'/8{pz ߖFF?U|3u:/HĎDGͩ_uĻWO^#Raj^uty?3z8^}7ύԺnv3ؠŹ윲aXOW%^;;ʺVy'sgO!v,xlZB)YBV^hpaUG,[гCfhM;ѲyK-vJʉe,..njgلJwrlVO|/_؟WW<̯bC}C_ZKUÀ=l|?-kַȪi벅8u/c8>kzې|?,C}8njw}>| 6gK;0p.['bYO㴣3m^}~wrãe\CE_͜^,""""" f[l\Plpiji("t{2H l6By[h3noױ1;:lòvFC:c/}NGފɮ(&%>)%f;gW;r3j`WP`RqD(.,%O$-ljRZ\B ߳"IVVm۶=FCKsQ1T / 0KT>&@zV%j7ô~+7T&Y#Id';< /4$U~+IIZbFzЋ1$+"""""WpW<2@.>[c1"Ǡ7Oin=RiOu yI(dr 6)ʮtGJ}*B\'F }\wXpBFݪf'W^!#tHY2 УCĪZÛϿl'79xk V|5}hhj<e~&sg7fwǟ #)%P6?\uGƅXk=K,\AȠ3/x{|J "Ŀ״augƤl 6 ~#kA|tdɉ~+()I1@뺼Lj(z{K69?]ڜD~9:5p\#d/9ջ= CekNMt;'򸑜g|_t#$ Yb&O1Ŏ`yݟW8p$}cc.!Ģi_~d@d3^ޝO bqysr <fN_n{0>/OkOgP^!+77ރ}::uP؋7|;qp 9+iu.v ۰5s\,|׶M Ou#Zrn9ȸ ױb.fվ ) yt'e9kZ>3q0M ouDDDDDLpF>w/%Z~x_z F>A-X#\z)뇝@4kx)i#3ogM*ϷmI۶yCiEdU88Fθ{ƣoU~6o]׃eyfm]1s[5%nFه/b^nj"f8eiV%(ŢV\1CUGVs<6֗#lg3]N#c+ w#t™dfn c;^Z>Jjl'_ei6,K./cKœڝ.=cKx)Ep_{NoOJa&:[8f"O9Cam;H|NZƵ*bo"}Nc{Ѭ/[DDDDD~y4x!W1}mh#cȕ p} ד^G戲$^нg3/aÎx,{ _oiΠZ`!OSd7qϘt}㹩|c>!CC9U*UhծlC |??ͧE8k8Fes|g7oN晼_x3ma(]΋.ssڜ}w„kpdVNrv?qgm1]{sa~)f䲿qmLcԕqi$v9tƅX0y2ߧcJ}p?qUWs8~oy븺c- g20>//Cj4^p7]2>dv&/7_ȨQVN~׶vw 7I<3'EDDDDW+}<\Ӈcc-'w96qT `{vwY(ka64՟#2c}frJ;c@;D$?n>J󋰛ON x5}:&P_Dz<ǷlуWWy5L>/^&URro=k+vwZ<*V/XAi!j%4MfE{|q}эךQZDE,]]PkI'S ja##hl^|W"""""P;;+knsݷu(sU$U;`aR6iIt>e;18X)NH|N/.XGiK8Js:dbcQSH9'0vslNѪq{r{rAu_aJnQ 3Eڜ~)'oma`p*E}$&xn<9k;TR.6x ,̃,1b:1*O;DA$5%or9l{3~`*@}0[WV7 :*)X˫<ʫU>oھbߪnpU<*jQN&Q,Cp| lN7`߆ @t3s;>H13WӵW pcd3e9׉RQQƥxUdD*"ٸlkІr)/W3qll.66-~)cuf6e+eT.~ dHd}ٶtцYLɪ5 V7-OIDAT>j~aAk ΢ )ag|Sx߃3â YðT^Kob8u.g:Z)3BB-ڴqZDc"L$A4$ c&d"QX5FSP;Fk(c~͇ITt?o\^דּq0CdI1,Y$I'`#iNRӅ/x6m?'=W?{` WLoy 9O=rްuZl(z9͊k^3ϵ `Ƈ'kQD.ki`u_~;^MNrIv>^zAn>y~(/m u(᚛#cOBP]/žs0O?  m}ױkU⵼bݗѲT, XS#aD[Z^y;eZmܸ|v<Et nu3'͛V1th?wý\npZ~hDϕ})#4i!nq!z7^BUZTK.`䎻obϛWGUEڷ;^|7 r=N(DQ@ع7ݺ7Dk$I罠:Tؿ؏3~D|Ӈv&x0ekSw,_0ܨ("ImZg9UҲR3,b@Y,xvo2gHh6UdcV4b,SټOr젧Qe9E̿7VYF& ,r H-}<#袻=[e%1a/\f& mQdYqJ.Wg -rgg@0^ݒkOs&OJ=tӰ" BY[Ul3йȂ- xIENDB`spectral/screenshots/3.png0000644000175000000620000070366213566674120015610 0ustar dilingerstaffPNG  IHDR~sBIT|dtEXtSoftwaregnome-screenshot>-tEXtCreation TimeWed 22 May 2019 10:23:49 AM CST(m IDATxwxU-BzTT.Uןvݵ.vW]]{]۪X(Ho"M: -^o, (@c;HfԖ9g{J)B!BqZ+Z_gնGߵ !B!8i !B!8)rnB!B/H,B!YX!B!D B!BfA`!B!͂B!B! B!B4  !B!h\M!bvvI >fYYY#֯[g3}>a ZѶxWX/QB!b߈O4L***vyRRcS]UݠJX!sݵp/"5-KQQ^NΝdw7|uBQ|Bu3p9УGw)--&1{}Z!hJ}h{o==^lQ'/73vEzg̖f !ZN:24jkj)))_F)9g{nضͪUܹ3Gu$-ﻗ=z0cL*+0?9LGnV LII)K.#;%_= ڶ3vܧ̟?˗p9gӡ}{ڷkǺuYKp/bM~>k*CGee%+VriQ\\w'69B!vW8fۗܜÐiGڵ} `!`0<%w1;uba 6e˖OP\\\속Z3+8Ѷm[=_z |~}ziӺ5Ku=dj8cBZZƍ|z8nw]~E-[=E߾}{4 !LEE3@ڵtn( B:˗/owE}.Yt%6VwM~֚ 6pa0X0S:t 01s&f֌B!,11Wg4r{HNNnv%,B;vOEE6l4]oԯԦ#r*?0lfܧn:ΛG}y>c&`ܹlܸN:t2:oSO?CQQkУGwj͛c>i{ 7p~zt絵XE6m8d?iB!:]Zkkm>uw;` !@YY 呓Ú|s7neKKK)++cw?Xn=OƎȳ$77~*%˲6}:0x\.W}12s,Z1fڑ3fW1].s W`۶ye1b02 !{8ÂW[ c]c؎ݠmv=dB!.25kۚ9B!A+>!0wMJJvl]-B!b{N$]B!B4  !B!hd B!BfA2B!B! B!B4  !B!h$B!B,H,B!YX!B!D B!BfAie`!>WPP@^^^S7C!D{HX!B!D B!BfA`!B!͂B!B! B!B4  !B!h$B!B,H,B!YX!B!D B!BfA`!B!͂ sV4DQϔ_MT L"!9O\"YHnٞj MB!bXغE?at Oءn*Z/@eYTo.q4 FmEHz2NPB!Z7u# zJ&|NijqNF1*A{gl\4(lhVE4쐜ъ#ˀgώ}?!!d8 MV,_h%)"eBRZx Z86 Ìb*amapLʅ'j6o!G QJa`*0CyE)p9.\S+O?>ςB!C`!SkV拯'LjM$ XƪAEx pGSRRK$bq?Ʋ(RHOJ_T<`azʦ9 B!B4& g˨E!μї,eǓ@Jz&]v'9.Ïu0MC2wv"PJ9$|Sp&+.I?cB!boG}?~8]>ǝðśsFq(*\FƥL•ajjysi.JkE(@kM\\0Ll w&Jn2y׉ӏƲhxB!ID!_+/[>\Au,ńaob/yyyzk8 j>]DzrnCۤa7LTi7؎m۸R\ay#h{AmOxV]me eRNc̈?0b+̘6={S!hʖLO&Ǟ5&xFqm|A޻a|m7 !{eL|0fVx}|g,q< |WB KAp8wS~&b$S9rS1ꍬtQZm;P\RLiYt3SZ"/#v#QP\SR[&aBcY>[eqc_x`ӝЎMԲq5^)7^w# ,(`SXnF! P(D$,lۮ(۶eYD"BP`H$\e3ޏV tO{FuśLHqmH]<0{^zWՠ~v/g:nx3VKM⚺k? y'h*֜rtJlju`: ,kZk," IVXF2{Y9:#m:3/gE "TJ?酊)j~O:CFoAky*~m&m5lVhFQ_ N~R3P IAԢgD#aG+RNRvڲqJ|>?5LYhd阙9_#|p:q&0{ s3I!h]O`]Cђ%[2[47۶L\.nb8\IId2~{7fBq 4v)] r=bڊb[+2_PU^a]PJ?㆐V$6ٱh 6~kxt w]Ғ[ ('NNKs`p!>|W^Z7Yݎ. y]JO>Z,9KGBwxTxQW)T!p-{X}sflt3薇w`"|X>U6Զ :B> rBƲw%mۍ˵k q0{9 (*҂ԝʮ.(12I #6Vo˔ˍ{][odyFiov_Ӄ47 8y5`* ˕N㎧g d~\Y 'vS!q6=y|:'jv<N t5x/wD0-*yһ\qd}In-gw~$S/{ +WiqGd]zO^Czx)8{ѭS֩q@9?1p@NI r< =[M/fS@Ǟ=H9C4onFnn7!hNI^?v`L j }wzFr{:¬/!E`ˑ*8iyr>fʂ,NGm8&= ?sf.@pv% 6`ȹCHu+T2&LZ RkyqSRa6qqX-_vG+S@:?N'e8FG,IpOOn9Z |WWnـF)kLBg; Xk&"K%QY6.Zc-au&dFkvԆXN~?M:#:~/KGпUܯi,>1ܗ$,~>3#cNK;!wsK2٧cKCy_ps#ioj6| ~ ;}0Ge7rV T=\[OVmp/3#.w{_x95bW0ntU:@Moۓ*ٶ]_yx0M+~4m\A/,뵿|ȨWʕLj%}o4E%3Y0@zV,+ 53ѡj*˃v0DeӶ_7ҽj)WTznB7IͲ,f&OXHE1\yگ]iu\M[TV//.~TgBbۡzfU4/puR(+I!v^=s嘟ykXv5pJoZ'727ůYRew򟸺_otw~]bl-S38]CI8Xf3q 8?^;:[ K43[&D΅NC9&ִjLPͦr ^/֘hCCz̪q@~xb8քmp||We{mCȶIpOqgƵ%ٻo̘tL=/ .NnvL:fy 0 md0ђOYԮ,CUېRFqm۽Hs<;*_ɷ_M`œWR S]\N} !-okN>H|8|8t/ ~~8C8wvgi!>fpM;YθƸ LSmG*@czt?CϟsrԶ=V3 ãt nZLyuN7p E솬Y[TDFj2hCjбncI9DB\&+qlT4"%3/>IiՒ#zERk~`*k6mvdAǬXrILKmⳳk<]89\p(PVsm AKѢˑwūSqeT.|`z n 6|gג7+{ 4!DSS$w<lpDD"dֺ>xd\eOdsg_@QLރs8-/_r6<}[J\d&p)M0%)#x庳oGi'BfIzh~2.X)%`!~>* "|dwvTx6ʮ~-ӨAS)S RwhGrTƽ;G:a%~Ļ\`K w~T]8)N; ~"3ѫDJXx3*uKYE\$7|"9tj)U_rO7jj T IDAT-,֕P c+&DTW|L(!3g_?2܂8.9t`iZwra==.avT^I64Uը4ҶZɄ_nvI|"Jnwg{?|θqe\̖/ |\zQ%?DŽhpG+TәNveL[U͖lp.drY#9k7=[5ŠrBAeh4nmHHO6ֶ=^W؊'x~Z.4L|lvz{fquj fڳcXID !>.Og SۮyVR1`YW0mrl\eZȥ70y/J5;ڶH YM-`0:;_Syi` 8U#2 ?S&-2ڟ0[xM+q|&jѠ q)}ٴ-0rpEȱ GIP>D pMaPY\JBj u ņlR  8h LvXYL 6~:iړ3]?ѺeOY;Om4lGrUI|ͼ^!j9xZ;"is/^72Ph _:NƖhfyY=^f^P "<{)ASIwnbKn=.%8Snw<&ʕїΣs;۾HT WsM!vGeohqS+6(h͢kY~-LzIKHmy؎᠇Ⱥo/yO<}#2%˄_ei,ZVHvs?^Go5DžuH( 6>r^/F;Ìeykh2bil~t\7INHrs"av5hjj"O$%݋Z jP5eaT\@>Oh`UQ܁,,&19Kvle0wpUaD:0̺W`tB[@?=fkb#9Kt]DBym ׿4mǐ1 xѢQSH:!>RyXqB+Y8æH5FCضl"82"8_Ù{ѹS;n/JiRqTVHnY]5La2nA NfY5 !{Dkh_O]ּ<[:fe50eYnj᥼XЍnSs6^Ą/&S:$($ wcjA+c8j&:Ut]:625vllGaf9`! p8^,/ gqkm$00|MQ?:w 8e[3cNI\@&>z!b4fߨYԆ xOx 4EҖ,H)BV^,d >/j2>} vܭx&y頻[VFԛg^ǃW wobYI-O9۶SwE6Ngi\Exg?ƃSX齜 8Gݓ;k4a7H8wØIYYRCQ35Tz'J(|X'5O{0Wo}@_1@iEmmRZSHu{Y/OLf/\A?K˹hBξryB! H2MF jmi ,=^xZ)(M; . 6Tq~?l`AqxlhoxwOApY5k,wg>H ^U^7ZΚS`&Pٽ\ltZ Jn"V+|xS^]P @z `mLc pR?9u->{>]E@ q&q1W66% KKqppTlLR,-YGl q4a;_/f.+/_u1[N<'8K%BqPp߫6Mݓf5~⪆Gm;M1&xsx|!f>+^ƻtX廌[+pH` //YՓ\="yq,>x?Le'swq>Id{p1}G>ca>mV6=^]`(ξ";4w$\qϾ>?Oϲ21<8S3 q9z0.u/:LTVnd}iMRNPmEᲕt՛M0OGȃBβO/wnwK6hxޤa\{9x;(.tf@lzC7jV1mIP̔oPv$'jp 5sSq6೅5$~)\z$]rZs8, 7vʦNL]lOG>i.wIJ ~iИ )&sO>+SST ᓻYmBd! ys aCq(q7\=[Ҏ,G)u(Ƕٹq1~,Ŀyg峹&ؿI,}nэ;Esb5Hޤu(gcwc1=>AZd堔tBqpZ7˨|):wrap'bڊ8i*C(3V`X]H;ֺAo3[e;^dbډVjg*RMYc;ZG [si&d5'aSCcD^wkuIm{\GT=$O2#KGз ޽s8S=.;k0םn;#2;r2~=&"~m8aJ7`=}/mejvqI?SZʘ !@AAyyyM !M`_jkkTDzN;uiց8TȴXZ e ʼEٌ>]^4M~/+UT5n >\ĒebX)8_KiY :Ljqi;BMU%!H(VlA ʃ@ ~w"TUPHJ 50LcAm(/& ql,iֵIN,o; A@ !BNCs<5_突P{nɱ"Z[fӹe6 /3 jUa}q5exHHNU(Ԍ!E;ɓrOJ[E> $Dn=m 4{@`!Bqih[ޘSW.t=a{eh^l(/odmI._h+T4gB!Btٿ1.њ7roSGnӤujs!70ϭZvxK޼13B. !BfOt$T|ym蕛[]R\2p0ŕUn@ sI,3 MfּSZRJ$=՞>`(a`u<g QhFf@QV >C^\Vo|o4ێ{ 5qYn:Bpjo4xC{W幙5(?À(/qqY؈.}fIs+79c5a^ZQ6]w#?iQfjSW; ?$³Fy Fucω.`sGy8'S3yJ츍FxnCĥv0-ԝgãǏ-S%!z/"(*euewoW }m(* {o!$L&[IBLBv^ϓfsϽsg~s8Oy_M03 #}F0`O|Pxz|{bm8~"+>V?%.Mp^Ey|)8sa9EAӠlėqH+!4JGyn$ FOp^SmSL*XQU`&\xI~ڔfzQkIĈdWV6Ժ 򂰹w񾇭h1BþyYIl*.:D,U쭮bcYCՓQ}ro}32;\\74s-|`D`EQ` @ui|i:҇ *`KXYer,ϐ@vi{69?Cs$.-xl7 j"~5B ;۪tLHt jv.c<5ZCw$6qx#4IZ/4Ip"<$0xuuxu$m$iCH¢mC0T8<;bA-S疙&CJJӟcK^}[BkCvcSm]M{ش,b`VO>T{пw#;5&L?kzndNnc(!+NEQ {l q=Z|n~`!/3 |x AMM`D/XInʫAR@ 6"Je-&qKAu-!H'j*KM]2%8hBogvri(-ahNZ 3(H<ώ't2 r\ɓ/Y@k/@7L C" $|<~IK5bֱurZñuq? x8C:N4;jinOމ{^}_u=^t!ğytjEQNT*Viq$:lCD/0dWt4S`Gt4I_/t)憅]B>-4xq_V,t<@'DFrTkyy|k~GeA*_45rBS(G#||*8tߧlxx{ẀeWs$W'NMo8JxY:ZŽ/wR+4\%3H}#&yvF9:&y|+n(J8Nǧ5U~s:1pwv2h"wolQB1vzAR #AxVc>eK߶ ?[:Φxc{S*$q$ˈǘ+i)j,s1g}u1[f2G|vY+%Qb1I|G?/@q3;ۃ[ >¼F*N9<W-1b0ӷOm,A3n{9eט޳7iIlN{+6P4e*$iWdfVJxEorIFuyCMLчz :&Q&;ɸd}?_?n冔U]?Rv}"A*r\ $kr]H4Jw"ܻur{:R[SeA^;C|@=s ;0|-8[ϯ}ߌFr {ŝpz5<gDΟ:6tR5";6$$kVSg VA㢱ݼTONʓr$?ҍuM1W}.[x}θ\/&o?! !k7oAҕ<Xlv@U1L̈mNIL"x2G0{vP !ą3bzsco޺ALћlQK"6TTS=('Mpyv</U(&O? 5bVV oCM*$&FGpiS8sd{YAi{?5zܲՀϖU3p ,0iAI>I p{1a7<aro/=n`>@OaHPREP #fQAvo,*VK|RBUa%b+gܬS`b?k+-5f Tr(pW󽩩4ĭ3ϾU)ɤx멕)!z/e:|:̠3IbC›a"v=%u 17ޔh$,fHǝo摘2]2QIj2E I`2y$a:?c,Dŏi̹Kf>Z6, ! @DP_bHH'+=1|A?7|ո>BEy>_z=TuEQ:} 2oM|&dXQ)@xͧg(|ӡ7qYOXUR J0P=8H`hx+:?.ѠkH= 5YcRc^u/R^M㳆/<+F?WQ4MXBf>àOZLc-ջ'm$7 J@a\p2~Nd[]C>?^܆\`t&%+x;13 +7_(¾jCljB@#o1"~qV'w_yqٍZho=?^JW7! uSa!%> pYʛ+xsѽi06woXRԷQZȲ,Q ؕxu-كB*FooVIFyd}R3uș|!wMHd#N%Pw6s!\8[)zh.)~ l1""闍#Ge*(rvXXa@[$I} &.o32"I:7l>;)*@(>֙ { *v:lngL  LTs, ~4B'sMY+n-NJrJޱNhl :$%7_| ]?3tKYiq3y?ig놐H;7,b1agIi)qkxo=f4~rGR*0GYLGѓYWGǫfBC01hNPmO #pltcM4h=#TcXĤКoyzބFSkDm,±#*$&hc/EQQAAt.{:o|9يq[& f[ʥ[m^#e@xu~pl*s-¯G8sgo|䶼c f[֖KI: XՎu7o.1s<<~ߏFy+l/yx?c-`z%߮|_I]M%GdۮF; ZY( kysrv|㮳R v{֤UOu8 0JH7HrZXXxX-K Ok\Zjm@ЏMe06mma9ԅD 6ת&l$6Q[&ͧNpa-4Z^pBiv\o 䀤ַfA01udEQ#az N C4 mmk"nRR$& @U,Ix.£ErN2бb f 7]oI (A vDiu]\<HLғ&s|}+oƆN & #10Ze6~!~o _d<qq`1qBf Iک~CBGGÓ(t ur6B-ө&gY|X*wwÖh;:%x7J,A>]&vyd1Ҡ s ^p]>Z1:S$>Q ){6Rx<( Lc8{Xk'B`9fSAd16?nJҴOr"P(L,\r: ,4eI>s<|?:U6~aK2\.o/u8ts'H GT?Ϲ[le~fYY./->Eqr-w͆i^&x/C GF 0?)Ӈx2곢/(Jg> Fcn R|$5-X[xi^%Lph1) &nVxN`j\@kئXO_ [~~τ{.pybGpkXX,mAӷPQ@+$D2VcI%vfvbfZm/IeC41IYumuTQ|l_h*JW͕'|{x<!%˦Z~T(fgz,Wc`|z#]'E92=6]ci`#q+*JXQE9$[7ڼ^{6T(ʑ16W2NG-44X,vS$}Y P^)J'`EQ++rHBQ ӇH;0%r4weYGmJ4vEQ:N((ٌ!Ғix^t]Dz50 L0et((FS%9*@y8 +u)BѺ(ʱL((ɨ>HtMhZ0 l>"pckJwVn`EQEQ`J~qUqS6®IiZSZ{P((J7+:!4 H)?)e??ҫ( EQY%bFQw>3WMutiLYV*O(Y#%ArIA| ! 3U(ʱ@(ɴAiE((BWEQEQEQN_Z E H$ᑆ: ՇBQB SPM \WHBBKvN.}lFhE9xMHC_@+k,ppФIa @ aEQ.\˦ ټq=[6'wVVTPQ^ήۑRb#:|e_E9D-(FeP`)CDY[MhX mbWH(ǍpnIu֬dElٸ1>DWJ'>-.SUR8N|nGNJȚ8 6&OaewrQ uL]M%GXBVSHtbh&.f(񫦺OgDf- B \=jM4a#`ӆuܾ۶0㌳HN9(Ǯ łŒƸw>yDQE9-H4i"[b(W EQZ[O>U4 M@`)]4MC.B-؁4$6e,!R1?;C3|D"|#µL?,s]U(5c$y5XQh-9.c-mN'.cZ}"qtM[}kVzS˯!ѐ:vVQ]VDMع ?JQU+Q]UNuE1 Bج\6=*rd`"2u^Qh{(4kk)uxئСZce:c>Y∗cGfzY SXQªj>6mjm \) ]0}K^bض#33 B5MP_SEBrBhԓ}xi@QNtj ̒5/7fb st-TgE~Ⱥ+$%;.x<^qN}48M[JQ%T]GԣQf)COFh.+-!L૗\~VQc'[I>v((JRѱ>\?$UA׉5ekxc=^ r&|/P7~J~[VcֆuYj9ѴX7>s4TI}] ~&Dm¡0 R4q(KѡEQ.]v֧෵s57~7ui!)"ԚNf3htz7feǶoP6r^%{9pT[,JV3.dSF\y,4Ŧl<^xc!c `p%<+?>~ӹyq3|Ӽ Off.Zo'K$O8k.DJRNuuaVXmYhm"4 7A1>$-)BdeP^^/9@ 'ӻE{ Q&ic?K+rXFl5O𗥠HWAQ.8Su9֤ᩎ뺴 )%?*22\q.B'2'<;ϼ m$_ ]]że1Oⁿh?@۞À31 7}_cf ʦ[LҨ):4Ȟ)inҟ_ &-oݮߵ 5LGnz=׿gO>oJ^{і{iw(1cMܾ#`U AzJ YH'-`ٛRtM^idgg ײt/ X{ ;p0شaׯeI'(QoE`.iU+A3(J[ SYѕMkNZ;-,.:w\Ƣqyp3f臬 M[c?aFJf>5w#_g1qϾ~/|ng d_q- }ZCf]moOؔ,/[n+2z\縢-VLcFRKoݼ?չ`X@ ~e6%葑AFf֬"'#arRPARS#bIjqbhA E*VE97tx"=9&h8].;q 7B5DM@T&MȰ#H駟N4fh 'L4%G ~tӋ:BizRXwPh~4/>]':߼M `2{&#y# Wz{(l/=~OQhץGMv5!"8J+Ńw"]WK&M`*+LMox<6j5Ӫ$̘E9-ƶ[f񹴴TD8Lס:5fQ^B jbhhb)MNnnݿq^pN ˫/Fx)=lݺ h |,?".vNIMպG `{lu Y\Nawrܐ14GGkĂп#EQ:[)}{ѳC{ghO*Hne;]In^'_5/dMtf\CX@ӌ&uГ<hx^D~ĽNm5 [u;7mr> Z^W/V x 1s]{E9+n1Q~M!$>Ajm!۶1^Xhm=eaY6 Ifb_I!f{@ !pq),rs^ayL粓5v/)kr Aͺ_/14Κh0!]P]drͺRuΟh0" a?N1WbD9oFYkV4`zu5KGTtF Ovܡ޿"W'<S'LKM=6nj:g틟K,s(Aj4-O!B"t&_#"&B橆8y9YU6Ә>`jOA%68z 0fFFͧqx~C5c1:y>ɞ]t(n91,n %m^XR S,;2EQt`qwV`ManKR^N:IɝlV-R:z5i%U5TK;:y&c2kxa]s $Hl0)s:fo?fEDRtjh/ 7dƝ.^&vR{ 3n`nUՂY_rF&`>~6Faed0!'uGopVE CqTW fZd.}^i2anpeeK(As+ά?VPeq>?8^a^ +dK8QgF6EոvvP/^k2.4DzImzmxvd !&֪31.D[.kiͻ]i:߻s`NP?_aje5C}Lb[mb.<~ɀˊݒdD5f0ޅ&=.+'q:1dM(rtk pw46=j GiZ~,޾۶8/` Ce\0^Q7k39'M _ΐEOTa62qWg\p;\zp; <, t 9̸mR|o>OwUEӐAΩ3A9[/Ϲ_<<@7WޜYp3z_9{ohh"XW揅!eLV˗uOCEFf&XR^YE8֣'PYQEɆM<:cc+p(t`hw'8jWI:u;mlo{ 'LX|ͿVu+#`K=2|\͢5 ̍mu=C"'gS dk'L YNXK{VDcE#⠏j$th=a.' _=ZyNiƸ@pl1Mֻnp@{1n76ɟ7j}s'p׿-66{>#K`y \J0S`(Ɵ_-x4^Ů%tK#C( >nhs6/I'l]gReSf'?^P6$j5'ػllm84(}%^t^JǛLEʁ}\6sqdS:>Eg~a.?Ml4^# 6Ή`1|{GgZ0'?EQ Xm_p׆_\]_}{zAK3`WY)~8%W^[Kz;S"ugesΥؑZjORr]b`nÿz] 5QUkxz \q>3#O0: Bu:WԚ^wc'M@Cw"f)@!Mc=]Hm+ԇYs3TKBx0 D9Pk]YaVpnnKjݾV{}X| gxv/We Gx)%lm߯s >m0.mGl$•+MF@&=`Puf4AǕ us.`;-_߰fyýdA<ͽH]Drq}hZWZn&pQx; &_ךb.;Ti\^(+yc.]kup`zM7m~ mI鞮 GAQEȅmZwwvva ,vU, bK,Iޔ7L͘h5 * `CHYz_:֙9?]]<Ν9m̜6_2뵇(8RUï_Zjwҡ-,,3p=T7Fi"Tˋ>q#H% HpΡ$k"Z2|v \@S4d $`u Bpb.ɱ$& @mdPr,>Ac4"AOTfw.F7uCᄆL!HѶ  X #Mmf%!>DpUN5AGk$j34&y ~0˂Ѿϳ/7xqT{j4n 8T*VH 4E5MH;\,,,,,:G'4I6k{wzSTuCYzNn5˗j3PmEtlJ$YlZD("Ԟ@*vKFFEv'S$dt/Tdܸ(RPL` \1r:*6xn'9y[$ђ+C<'Ma~׻@C{d͇aΏ! ) XE^;߿T1o5֥!~p4w2xyL{^<с@ =4ZɊCp_2;¼9myUW,Cxm9]=˛\`aj'^u' 2rHo?TqiE+ؽ8ymlIݱݔSkUbǁ ۘpUrLzg't,!ĉe 'qx_Wa|?.δS8j ;сNZJ*DM˴u2o{gxh3#z;~z3¨QqнGGtúf[. 'զ`NAZnw67*̞7Seӆf %i7PM&+,_bݟGUauNSQb_[@NWt:U4ڈ`aqЩ576doAon6 S@fbk,Lޭ?5dj :@D,Xvk٘6=;7k?cNV˫KF-#teX ;:؊׹(BVo+Ⱦu%F"5ߘ(_CY:ʜ굃I K9vm\ց~Vf_r?U zNJB;Dvp/az;}/WYs$՛J‰u ? rh}et/'J ٵck(*HN($Q4U%K&]ԅW&R ]g̸Q=Y8\@QLӤbF%` 6Lܡ,_,] NRArӓC1(@iԘSqCA=]# A Xr= gS ڷQNg4/|!A#z][n"*(2o#Q/&% miw$ԄPEM:iNO$ 6 ~HCRSnzU0 i eՄϿuE_"-kz5sa*PҊ:^]aLϱS}#2%; .rcs8#BFԼɛ0LPUQpeרtS#C~K]*hJ젩05'nzޛDÅ夑:@]uq4^W{qi`*v }MXIQ.t{lBū)D":{!xBx4;l >j HՃ%Ax7c`?l_:',.gȩ i(D ,g҃I\5}}Zu!6D&!݅ag n?\dP e6*XܽÄ?b@0?^fP(\u ZC~>':{-S%!U=KL7dP]屻!,5 #,Zp]N&C^V"LmgWhh U,D"oQd0lLpלܖ!ٶ2O>6 p;W 8&  Ƶ?יDӫ-&O R+vX(6q]&/_CzPo}?4"v~t.0+"<8SrɋTx5~u^G#[Qa*[; RtZr(†&CmgfQz@vl|c%:sYq%PU,R|l"N1Yb#.ODBd^#wo^_þS<:cztzo tU#SpmW6?@߮E$pYoBJjHhGeIzK«:21,/f^,1Fri 9!05g{BČ l͐]*,gd_l=N7O-KUDSbNLs"߹}A R6|b(e ݻ;u†nTT^*,?@АwDa(rՋ<~/~.Iϴ^=n<. Y粪B!BæE惣dݻ3ym1B$\|=LIM`lp2hNal'Z3oM߼{ 8 "A# Վn ?YĂzxu#.'3- |g8?MrN gLϨfكz.|,Oǥc0(t `}{wl棥Yux's^xmo]OY.?UOB~.*a&]z)21̤:.9X4 zgR\᧫anNcC@ ^ߥubW#w2\ܖkR{=+D>f-I)᪯^BL7j|kd7&r  q-{9tyymQQƾH&e5S+)j\-X.j*1('6ӖO2 )h{K8uUݸڷ!7wW=\j6;GJ3KK"k< ]Ã'[̌Mh[l'5 z^]b5OJĻy$[;Sxo9+/)~=a=bSѰcIHȉ8Nn'at'-5,It*| ea<ش4-!v.$`Kau_k#n7Iʠ oyl[8?UwiN)1\"ygNP9&> 110M6z`ֲ6g rj1 *U~{ JdqPUˡ$dN<2\ 3Y 41աxቍU2#J_tIMnb,RXGODS ́M M,T'쨓`JVؘ]x-\@/sev:G~:m3810*()* R(ttQUNp$ #(?aR!w21gRwSTD?ECU^B`XcQu~ SISaRkΨj0` BF5L!ƌXZCQ${f_87¯YQ!0<Θ`KҺuSQ Ng>f6Td0zj_mp}#!i=IhKddۧ^C6'uwBETRSl0UΊ)]3g>h,-#vD`Iꜩ7 9!G ɗ~ gvN&/e 2m :6o_p}hT5ZW4',g85uS7vP<8ݥ|''oP ,DRR8\lt)'e!BAj+kdx#TuIcu7. !t)oMR3i)"Ou[Cah#Ix 5!B Pޙu4bХ 99c~)nWJ=} _k;K6e6?\+yr;< {W|nibO(1 ԚJ'y#~rv-ŋwrOo']$K? ~%?):T- 6]q*7 Ͼ>/3k,]d) l!IuPs\7o4-K~Iءe"T1V$41j~I4??̂SG)EE $\IL6#| /`#4-WCЖOTYڤd#KB=sxIe.QtibHm&pMȢS, MI&=}d#&6 aG#o)0FK^yf?j~n.6N*$Hti j^|2ldymn 4R3V {_Veg’C٥c;c2ňD05[LjVYcL,46A$?Cв6(6l|Po ߍM$+Vm$x1]l:Jk%HNp !3vߠ-ŗ-*EZsBe_CS 'f~<еMܲ.lV.\Pԩ-CZ' Vq9el. _ %?Y3aIUS:IġB <OLV[Y[(vm' n Qkg>ț%pK5_qr]#]FuBu 'VׁxSm)G/J6$mx:gHRdE؜c*ֹp@_dE |s*ii54ʪs1®Y⡇'zơS%^#_X5;ihw}5~7~y(xvR]ȷG>36$ftIЋ隑EKHLpAKM L&̔d$XXX#6L$\}v"K2PmKF2$Mmk 5d {^vIOaDJ!IQCedW=dRz4j+l*l\oM՞A77tS}{#e_jqAH4O4jh=.k%RjT@®R6̺:\'Zʘ$"mX( ú v~aemT ՙCQ/ktfm쥒֑тqg?rAd':bzHD>[`yMcF2&x U 斚Qܐ,^k n#=]e {!dIu'eYkh%UFڌAΕou#u6dHNDtUcۧ)Q_T%>I]OA0&6lv yoRx uFHa㌾yֳ`6p-UAI4o}5MW3)6줪1ci$qEm큘~īr֋)_z4P:')myٹq/~דZ_JyA6lA4SGftr>ZLɌ N)=\7bڀd4#:Hi/яԜ08}-Ր nì΀ T;E]%[oe,gmDos*R(cl;>*_{3j6/Y1Lfu=I-/߉mϚgփd q)(q.;wk!jQxh$"Y~G(L?[9j54ߖ SEسm<>6_eǣ PM;y47\1:xnʀ*BܼS=_nsVֆx/:.S.Mֈu]:3WB@ ]oĬ0AbS!";u ^!~StrCdžx%?/CAv~6(&NG>Y+Aѻԫ?m6;S  a~N8STߞ-:3w*dU^go~ (ĖiS4 P 2e@зB>+Ι܀ )TABN"bڹE\ [[uUqa.vd̿2ꟙ_\BKo磙*gH IDAT#QP/k3/A&""b Jkz3oݦ" H?ء$alņǑ33x7YžU+XcTF>5:t7T9qɭ8%%BJ109pl3KV~ʚm9=p^^B8fCد0k|w" 1:^q{0ts<{jz,?nq|&[oKN;"A-ZT;Z)O)ӹS/!һYr% =Ckh[Wr<oaaaaaqޠWiϢ_~b۟Xl RɶbѣTE$trE7cv/ҦN1m0te_q`XR#͂>2\RS nܙ_i^Vصyv}F?RPϣ{[eic%׻@D1!Q3eGx/1pA`[nw7, S&vXzs0XR٘ Ɍ&4 d]XݞaN^'IYYS'li1AZ Qozf򥎿%"5Ԫ $;q6th) ~<;n=@9J]dy{g~ͫJ}79\G o97_oO_d +..Hj`45W4фEє+6uUMUT/ۣk(bq򱱰83l]V'0 \pr7 mYA`jS9R*ty f.fΘϪj']5!gwMFn;Pm spdayE}o"˚mBzssհprrS )xd-/\V;TPs^zz}sȵ?kM.YoDz:?^82$)wN;7mTЬ(eK9]M뷰8hIgټ+2}+"ػaAB,ҾմdOD5lqvX9BuxY#E ҇\#SQ08MX1+$ݔ(h'Mt)Т&E>dp:  ahN6~ּ R.g> Ô(#D} UQ'&)PV<|Pń]ϜE|{R.$aBrn*۲9hQ_F%ߜ70.Fسj{.ѩ}Hˇ7,E Q4arbP)4nppNBݺr3¢S)ӕu[ٱ#AG+zwth //Q ΃Jz(ёfIMUWqDu[x7Y~ǧxCPѝ쪁ȁZ@!p7rsi(”œӍv֓ZoWXžIOVʩįfuEfs< >#,[ɬuqQUqG~alY73Ťr>5;Gb&`K6暻o2or7hu;B^/NM蝠^ʦ67an wQgrqzϊ7+ 6(5E/$ R?Bp$D;Y91zF#eJ'(3tgx F o6h'pDj jnbT vFLH^FM͔jEL˨ZXX39\~I_VMȺy3y-Vnnv={?.%\A1vHzY?,,tX}k*9R)$ІC`(&Gհ;)gsaJ%9v-;A`PE_>Pڏaɯz[%#'<-FS{wc6A8?VaPUvv1" gSiEAS tO.NHI|}1!Yph^K{\;efI* }6vDї^.et c!^Es8C'ƒf*_w,ޒA|-}@B'-,,h,|B;}$$Ez, tRm')Mb|[,=uԆ~zs[jVJ*%2X~;G؊&x\8Fe.3TO9Sm8TPgΈ2mJmcŃwer^N%0ʋ8: &)^r jwMZ_w1Rr}u(Rr=WӵDIn n7O}疲XV?nJ(@fOrOpd3o7((`Q\y\w.X3V, / % vd0kVc*lғ|> I_AYEk[^sM){ݽG2aLJH4#T\%ByHrٷQŦM?I k룿wI|Fv$zI0^^ .O+:rJbmd;+1iggh֗b>9INX. >w_scN| _O4X¯ŗKlaaaq>ўkB.g|}ώݷh L2'k%6UK'&MvxNHz&\DS U;=.wxxX4d-~N_tՖ͸ѹ<`c]s/7g5.ʃ#SN2"&{g =$;Yr3i)(N=M n^yv[#` )qe=>퉚AI?פ1]u!p4$-,,LZӲܩ?ywb݌19a?A%L¦)i jCkuI_gEڀN7L isNJRyq]99 EQ$"9HraLscwN!#spޮު7DDpVH+, sa2*._yy( ?AAt YH"ui T]+1K^ kM+B@X.nj\q~􃖜9V"H@ѕ_A|L$nMUáE$VT *@M| hzyRѪKxX|3TATL U+&.&HCEE+}#c+U^zE\dY@Q98ᨤ|8"I8{^KߢF~D'ڙy|[e?tUαZ^Ҷle[;H积)]!9m[X"{85;|KYEOS}%>0I[<uu#N9D"EÆ.Nn۶_eƍȫTfBs(ޘjr#&**z1n&ݞ:"ϧANjw59O-GDsETm) G@K$8pzH^UDU"H$'MLƈ%iK"ϠkXhQ[YZ\4>AAWsqZ/90vifa~cYf&i;ua4" ^FMTNo<##f| S tu %rX0s&/@QSN2%w=>6.z2ɰ+9HW#D"H$,alWC6y9} 1!4jԭC𒑑Ead" #mSߍZCZ%Wf5ٖnUu=z77*!LLzT,M|3c<6$cO<Dr'D"H$YB jlOYpO7^䎌eʉ|,TK%$HqpFE.vWQ#3dGe[h+7sk7jEvOd^B\|,q!rETHރIlٓGHC#9HX"H$D"9K(m%|]ؤ4 !62y_vT~%)H%X"H . _6Xhr({mﵙ֤_K [i4HD"97TJ$5Qv5݅4H$?]iɔ&;D8'bI;,.3t[H$?Y"hBK冞gm$H$i)+L0OWޕq:앳Y8}5j ٻ{fbr)+w^ku}l2xå*{Δ7^_SR'ph_~koo7SiSq-u 1{'ye Y{3ʂr'ywf8z*<0W;7E^~ҟZ"=ؕ)* fEe>Mldnm+m/$V[@d]ŃԒ#JSP*'E uk'|b4긎]^g'`|ukTQHUSfVՎi*W]*j OkW4Tiwlς?$dmZK(8f#Y݇ȼ*֯g1{b*_ "u~YçyۭTIw]m,WĞI\wwf=:67/eK>z)ENV`TGçؿK,:J .m^_LqMѭpaWZ1l UəeWq)7P>,hPDoQ8nyJś|<oc.3qX9ol4De;bUy9-<8Z^R :2V|ʈ>${/_1]. \3>Ͽ6j:ޘ֦WD%w ׎1K{7?؛ĢO?1% v=4 ҽ.*hPn5VM͚"TUhJ*QAE+L%(a]t:BWY%Kg`gɬ6J: {B|WW7 8a o1Wй ZͬB,9·VD%b6QnWߕ1NX=dJͅG,2*;tOe6ULiFưn:cv|_epKXdS[K#^V[C[Gu U&2 [hʼn_Ëݪ)#F^:}DfEt@Ic:4m19 /.m et39[eӨI ,l8B;ije2qg@6?.3N!>O~e$:6H\X2q[47cGubb#0&:v;7?M|Q&XX@~H'*:W:B+*CAXUc-+ZZ,%.),i9QZ_8R_HȈ">(Opx+[&!4 lBh:'7/#&׾Oh8X![3J]MȴP4*UAX &.Uɝ$C[(5!iQF-4D;A `=?Z¾t=nGor1q<02/V Xy{`xT}oB4-WdH\}s ZtjwvZAV*1U3):dd }LE/jq6tWSKS\<[l \{;:6 \⠯̯I Pz~HӸuN|ņl}_#Rht+2iTty`^Ad/4h^zGy*]C?,FMT UA52ǩ ?&f`4.#.<,}:F/OK0UU|z~ˇv՜?pn|_Erue4> e=;L~^0^ u ys%߾㦲b0*BJlDž2yQݞ|$m)s}P\>]l |r!{V/?7G‹3&qŗqwuI>p3 M=Se3Xi_Dۿ! 9IEBk.sa\RFw~0\:N}.!BzV.}ef?Mqc{OS=x4}]Aw:/潝{+h:9Mjk,e11ꛌ A>Wr[3y,x6:^p9=/v/A3u?%v^VZ{0}̘ pªz +OE,ơ&E`rnNe+R~&S}! &%G[_9Zù!"%P_F 2+/ !;޺UM1CYdܴH(hđ›C!<2:M"fЧnZɃO~F"TG6+muTbR-jWq7Yw"*Q =d >$,mǺk| Hryz]x]ֵZ,)3cxLFaPmCL)o8dP,_NӦ[x o$jB EA~ ]e0lg*CzDl 0G+lZbh'cӘUcAйRl^ c^w{td3_<[~NpYa23Zcd]!-ޒp>;ka;i4-W5G(xy25~&xwFǸˬo({F6G:,ͤzYql~_Nylyot]t~on[W^y}_M޺{< VQ/z^dm13wsLyspsb7|k_5y{JAA?jOݗp[}՛UV2is0(ppOH~뒅V}MT˅:^[9F3j0f9((AZƽ<Ɩyz&~ww~Oǐ BRyw Ӆ$pNgub e6Co~#]O.ExX~;~4Q O-|W~@dY)~;_E'J6~yƾ_`ԓN7r&Lqu- iq$e݆VŊOA*5LJwiM}ѭh ot#mv/Yd~m0߈+-acP B0A[CwM/y^₪ˍKY5{*?3Ϳ*;(=BO'tu#XXXKS)] ,9$3}pm"R48U:5Rس"KRwj4Ҩm(Oݻmr>Avc/4{NRx8_3myVc1c; A:bY Qf" ]/i@LB1fU-~+PyZ+@JDv_mz 6Y%xY;9yIAiaV} 7^:MXQMt%ï`nDVcTE Rw)AZuTQhXKbci{\*mSYf9N% R`у#<҄l +]\e&! 9V%?I)>6H,䯚@wFtA$t抮fN__QKS)p97vCS4^u]Ѫ,\{UbP ݟj~^ sŗZ+[=rQ' *1&@E@(J9 j2/BhIt$~0PUEQ1PQP Z=/fPbP‘+@d5|GբI-7R\ֺ8JYs8v uϝ8ICkm-[h'(l+4/C1#[7qcΣ9|F5Ս"gzv* idUy}٘e#ʩ՚:4ieT@֌5}lX](C WV7UQ#i{ o-Yu&HTP[`/"3Q{CQ"4 LVLPlœ ^0S}\;1$gowPd<: n'Xe2Cp-J]_ٵ%(4܎do( a(Ҋ7Le6L- ?7o^Qzn?&%Ϡh`p" :XAa^MW0l} +(QĂQ+ t:PN3MHǥ̜ŭӬU50LgQ\lD|"!1Hx\ T/qI$)ydX$ |Gǹ`(F!dGG]!,bа1Tճq1ZW=Y)9w .`拷m9}u2K> Vأ*1QϾ,זnDUs9yF$Rb7yzO2sɾ?!@XJ+*ɻvT-;DPI-R+QǓ_pI. M+qmo3}U+?}5MqB` SΡ]9/?'Jū-6uz^;q`ՎkWvZ0?O_K;I;O}t(5 [~fm1ٖjMgo)aϖ%Lr \-zT{<3şw ||7m( hSocA/`!O@rڒ'daYh$G!HX9k ڶTI)XYdfY(TJ"9/_`k`ny& nr;j9@Y{L^M \SxC0g~XL!'E)*Tj@P3tR(*V-O'ylM~NF,+`ǘ)ѝ UܹhKTR  厵j)xsB82E\1*I@ y ,; Isɐ!( ?r?DrV8bk5cfM_2ZT;ALvnA|XUxdq342ɴc&~ҷFBtA<8b w1} d9qEhU\DiYx{w|%@ҏxk~6_C'uoȂd5^*ۗC Nrr㡏>aW8]6YyVV>Lc#gr~a<بؔJ~B^WaX4_7w=ހNXN; 9w6ըqdeͯK2t~ ÆҢpCBRAoM0 >J[Ν_`^M!cEIZofA$6&+΍}5R*T\_-#ݧFM-lV֪A7fnѧBD(V BаZ8i :wvWI@U!*VGʱf0YƨN*QJxKwA6 t284;Θְy22 lVu.05zVp + Zm6Fc+O1Q1(:wiUԱ F7mL4LTE=4,;bIʅ=t8q[eVQF GF:IսS~G UV,X^*G7& Sx#y5wkSpUI~dRte8~a!{ˍNZtmr_o".dl*B;f`>){◉?sy. m"tZb`7>_ PplNxÖ,1=8U~B'#:%`~=?-1 pDրU\. 2fRʌ_xeA^~VڭxL;DšմG2TmdYw/HCvc39P` g2/,2}÷i8SMX^qMw^ o!~!طp<C_f@.*1l \ 9xvAhiaM mN˥SY;Aٴ}u"ת:UoT4 V]-P57Gy#|ѣbqf3OGD4Wo>9>ADL,Q`nD+mKO1{TO~>DGE*] BFUa7ǧFBGp+PЇ/Dq%`n*+D9l.[ 6xqr/s`Z+K 2Mf ^A63"FnLpꐶ6kJجXo<4Mߐ`4?n w O\bbO Ѕ`KV4&=@G,+6.ɃEY`hͿyju$Eѿfqи7Q<|K_*nfmRFŲѢQ{Y0+@KpmC,íx'.r3b|C<>X6~ hw;m]&3v)Dε+]N^gc]ԙgewd|%Pi!@Ko([x.0yuʛÝܱKEJXe{ 7(I1r~J$]0-fB\'.G7!bnoPz1xnRފpDb`dZfX}>:aXi 6-z+Qh~jËyiX2w?Lw4 Fg{6q^_~st<;\Q7ex]ѝ.ڔKyanu9#ıiR |w5IBQ)}j|{|/ɇ1_i^Ύ#\x_*X8{.kn\/~q w~:MPpȰ+uAQԣli4?_ɀ Ǔv><.68v2mrH"?66$0x z/ gc8*< IDAThN1`珤r{seJ.jQD> \إ 9;>m/.K`}y>u-w%,b+c4EJ%/LÏ^'xk2)K<Ԕk?_$)̿qx9g/-Ӊ?vs&PZطy.BNSThG C! ^kRҡ5MՆP pr(@O;.m?"B!AN!UF 8} 2֊ 5`) p{"WI;'x Cl*sPKb" SwK W;MAޝвGcCEnۊTp}2X!3{/ 8 +AXa9 ʿGM-BU8v:Bo~u=b=o8\8+MAa,2 u?u3=ʌ{ᲂB*N^zP"BdPFS#]8%!/(Egڙ=޴s4a WAUli.gQ`aRCM8J(@Hu>#3˃H[ 157.65 (w6!Kw?d*$?;M !?Akz]ԯ"C~8X|rR K 9XDW|v_Fw:Ks[A|Q{Y$3AH A2s7ć-^⇛T#]&w'//B&1>W\z#P kV_H`8& mZ\l`,jBcH$ V">A%" |ܢ2 xS({%x Ey-K ڌO]zW==F($AAFT7U fX=G(̞2CBx]I\GC@3P) Oʽ32/Ѳ*WJ%Ȓa%%Hp i}h#K R(ڑ_T&.RáD'V?UsE|E5"HH@I1Hx YεQ.8UɩD&$$Nʆ@wSFi]I)mS_U ȧj5ʙgTfADWpUpT+Kňz[ GV ޱ36r9M^>𗪠$ը^E\r@HG5_8]XQvq p\|'~G ;O5Zs @O.VNV5Rx72DӚ*:vADr֑&В?:(&I " lg1ϕH迸 "LL1 $c8˗KFJ\BLi99MaarDcaWO''k*ڨl amb ϷhfI /R"H$fl%LDQ)掣H"9èD&<'Uذ)}#-fl) ҷwkyNlD"H$D"s p*hX}#^;Ssw)NN6D"H$D"8auFͭ2 `%"`&G*҄\*SD"H$D"H`YNKŜcd0[ra?@)}%##DI9 vdJ:U\U䞖gVSrh'PiX[ FHgpU.^T[UӸ=qhtۛDrư0.yt_p̪>KK_aaU_s󿱗sĽGY{V7?}U43Yt |.pIr@FOB;M?[׈U|JyU̲-G=k7m%uWN<rmgƭl>_ق|2ĠϪ˙p%K&h eaƭMِInbؿ}mXo;#BMsz=Jټ=9mCSӪk, gƄ홉9"H*b#B QG}&$;vps0cw,64ɝM-Ezu(zugҩ~仵r\ B#<%8НS'#HE9Ed)", c +_va1+F-C8߼?'~Y˄Ͻ3`՗<6yU?^"G}]Nj*iүK { Nl1ZL>Xy5ksuz4l;-t+;p0~Iceyx;jG a֮MBn49npAV7kVDrdoۏCj"]WX@ߠQ71q* 5(@Y<Z\{#ʝxPuvuӣ1ٳG"  ?C Q#iT>Kbg&r~1=ܴoCF\0hh 59E2Vwr{9j$$dLXf(iMQ S~BeGؓRhbSYH`(A%~8RLLcG L 4]Eq?Dh 8gfX$FՆ6|<7FA[S`Gec}eS`9vc)lkDF,BXn%PuO1{.Z\ T\@6SG!AB["]&1wč%kf-f$ ? hz}$$4 \D$юJ}"B^r<h*e&|Z$q4%EsY!dERuSQE&L?>+3Jr 04yr.VWթa쌍z:kflzp^d2͜YnK@)4x&0>;Y:tt0 *  ]uᇺj l ]-,un5Ã0 ;."o"ޜ/iJJlpb^todLӊJ>4jK%lL vGAP%"Y@FQT"+`y8k^7qEJWv,y( V"{Zet2¬G5 (0E3PHiQz<^ BU?F WuB s7b@O@tpVlBZA/AtDx vBoUrInVڝT~aJr<4~=u1~5uoɏ\fL[`fAB]}-Z(Rj55ʹE;tF5KL6Te)f(ŠB0b68֨/T .[\n2|:O _dE^RRc6SC:b cW+-QDk\Qq m7mrmHѳ gfAU 6SNkdt5!fѧ6U=4rך쮩su ж/Q*v9gˊO#{Zka6ϴnIS.:mpɗ,2V4ӹJ&8|u&1FL+LYDvTXʦZ;^r|"7AcDg&[L\omosysxfj{v SSg`G.g|UEހ9J %T-4i HAkꮊm-kYtu] . ^CKԛ{ʼn Hu_=9g̜sϙ{wL QihR%ٟm1mš ţ;SW@_e}^ͮ [\ W ڷW_!ʖ=`1wŞgZm3(UkFPmAC*Iɦ&ӷٔ9_Uo0Y_(I{sgQ&[g̫(+y:ŬOR5j:tLjW&2^h Xn[So2^~T1|C~OW q9{Ѓ<KlOcz =|pmԍY_Tu8x6c6IQ3w~ǴM+A"v|Ȉ/wӼ0.z˅]RD =z/V4Q L[Jɮ/g?~ t y-dQ Y;K ,Ws Pƚ'r䵔KoOKOji  35^$Xu5Z]Nno@מҤ_Len$vMFp"6Kt!K*Q&)"w^CuoFE&=M>{^y=*ãㆱ !K6Ks`n&+@QD1OZ%QOh/=GY)J(XϿZo,׷ff7xPw5Q9y2dBLibE4`\VQ7{^ŻDn|͂ jc:jnh(?_\GIr KR>nQ-Wnˋfw@ ̨z%*m]tŷ[Kl ߊF8?yX.wL:&?=T*ĐOy|F6r;> ~ʚx Ð1UcŔy, ?wqi'0+K: $E[Llmѿ oM@вF o[P/͋)l]g2rZko!jf0oqUdj)k-yTnP3.X|@X *z'0r7z#Uz7# #Q9d6u󺺙/- f.^X:r̲i lBbA"hVTbL)B U|+$ضIK(dɪ=C> Ǻho2crxKf4.HSX,ρ\<=BPbSݴskZrt+Iʹ-J$T/׉6&[[OoiL۷ŹBngQ'ؽb0df{WK$N< ]iU=CǙ~X@jxT(YbWe`?]#Ǣ3 .&|^>;<ͥ>AtPD 냙`ɳ7pcWz3 .!mm]ySJ&% sz[ozE,:ai1U+<9I÷JO^j xcVz}*OvFuȦJ϶?/[73뮶M( Pգ >}A{=ǪZkp!=eg'(AJ&ȣSg=^E:#nZpNFyd.}\ѝϚq,oj(|sQ۳<%4)4=wct6Z!m:ȠZ|;YE+ؽ+CFzqxO;;#woBc dmj1z_?L+c &3[cЎ߲@:ND"])H^OFH׏/ f_Ǯ̴|8yyGLn$v:EȚVWi0ݷ|Nz9m݉iV nJdt}rjYn {[G}#}۟ɌWAJ:+LmTSȢ,1Y9H ~3e0%&9QU[vyMfUF#Y`I$kW*ձY`T@6wLXt,5Vh<<[l]{E9axkJ~y|J{T l 2 UzШ䆩f/pɎn1wI6?`c */Bd{9)ʠE6A}Fmޜ`5^> 'Sc]UL` IDAT%T,]g9"RTZj6+r$zBlDf~IzBMa NErmba[Ku {GƓ:}| ~悪eUh@| ĞS9G}uĂPV͆0UiObNThKVdil~ȁ Q(c+l?}.8af;V_6 ={NHJUH*YY+Wh":r y$">SyTՌ0`a} >X~,"0c$"ʊ):AeR%sa#j%P%5Q Hsl_(!q+orifWJ#>x$@qqJDn>D@rb@fw֐_ğnkG9^__rl\DDIdRALR ?y> |ȝ?ʭ>΍~NvB(/,D5[+, W޹j2/+_Ȣϖ W=_ufnP;{$ESVnB>fZ6ϠcohtH9Yb犃wLVFRu/NYa5h2Sj8N|+Zi|٥1BLN\{K&W䟟v52J )^EwPh|_x?IjǤЇMơռCRB@4+%10-I_m!9. Oy*5Hb-B¤D"1 `9ӽ&Ba,(qՆ2cD͖V2a{#_P'\M pU"/dzyȿT_ .P M@ڬZcsW0F*.B@F Ba@N4K476Oa@w2)AH*N;oPEU)ޒjhvL[>$%=,jKOX"@w 4!p ͠/-j mvoLе- d̆~.߬rio{'<5"|sʳYN H%,1uD>3~FM|N Eg?uJDFT eeA3G-Lo1`Sg$QM$zJdІ;oF.#u\ģ3No#CJeKFU.{YU;$#3T lC2#E eG$eT~UžhmR#.S)H q?w0.ZBV$>OJK$1oFH!<&ol#ْbIL 83:N<'6gjULIntc)7rA |6n~<(hh04E'ڔKIn$2V #gHr+UɲyxJ\yoO ^+tmirFJ$~BUӻi^bGu6Bc|vkBdsg^ՔVD1WƖnc(H<.aeJ3}\ti]|; ʷ\3v[}>ywIfRSHmE%r;*F\SrqmI,%:O:zE]8W7GM/5!#C;Τi{_7KVތ_֙4{Iw^bi,6ߎ:3QvtyTp:h%kyju -"W䮎v)JmI6iMizuS"  !EdNE|@| .mCK`٬Aß6gYS$E sK k}-RZ wq]ZǮj@JR/9FuVjv ![^5-XOEvFRR nvp}QTͅrfr }Әa\2O_|+ϪuɵI 3a-CR]iP7YnYyE'aAEy)-OeJHczite6S> .QRR}$O)FDD( @03`D8 $M,Uqt\nYV|,/sšdH 2FcLNsHYδ!CZewquyt|ϼXGP(쯿C&dϊy| ~ x(Og,c:We8VCw[ǫB{WF:#$K6[TEXuB&zJ2ҡl*+D PtTRO_[Bz !%@\=QM`6#MjiY!^ tF)t=]SSйM g4>`@p)4ܳ.!Ye6.P-ReT&k wZ4VHuW*U ɑ ,,Ij[ ++d ]JZ,.W|FP m3$]`2k༮ *7Sk mf,F:UEeWUeC!g4fLmйBlwK Pٓ0%&'2( v,Fe*Ĉf.:lyNh_< ) =fԮ :|A ++ף]74YyB HTF"܏ ki5bH͸hIz9K'rol6}._[/'x.R.`X5E>6j8ϣY^WDq#>I>zDkt=O{>mʈ)sѴ>o íIf,&7lK;g0D Lmz qח@)[hmh'`f<; 4: ;$iZnuѸ}S"lu'l۽6OMyf֢߹-YUiB_`; 40GH I) 墁txio,祄Mh +>W|2*֌=TAecY@սjЊqgj8YM gwxg:\8gd MOyUGGӴ@&^؆n:vo.Kmhr(iGp iԏGƵڍnʅ~`[/£!=1k 'UoEC,L [`%V k2=ȬgjYXѺ#M#}?*<,E2I1 Cgn w)33@07/´%m fI.v~a4$ ךmS՞ı'yLKdaZF6dϺ o]58]0qO{!A%@elH` 7w׸T0 MxbGH+o2kr!wIʂ-Auh/c$ <}“ժ.m#(AL&}^(7N6TUw}TNcN rpWpaY)pxSwWausv-zFZ\zgmfO圗 |7/2@lGg[4M tv̾n!uaj:巙F>%ٱ8,^bpp7t(xwpp8{{בWPc\FēvԺw 56L!:Y;#* 1xrŌ.^.̧PFS+[n4ţG{H<ϸeLr< _3FLѼ@2^ȣG(Ѥ$xLøe),vG㭴$w/@aA=XOh<;ʜr$2!H s LsSPA~w]H E1X+3DHyzz3a-VذJW蘡"K6ABhqɽCؘA?g[Lk!1LE N;XNqG&+Ň(2J wbXm`]J:*."@y~ EW:o9*VMS^ZǙRJ _[Mtk'ϧ BQOr2-"(MbZESQap|XJh^z~%A(/:讣` \z/cA(/40O+ ;R\G"ڥ e~LCL=n-'or MtX;:96 %[5*BI* $g煼w^"bj2}p٫gӥ(FkT(LWĺLRQyGCiYoӥi*R`%%*jj% K@\@$'xKGS@ x+TuA$PX(7GD 9"Ri4'  in̅*NMJPJ(Vx{h v@R<&VQUBk{ey'"e[c. ThnA }h8D (,'9m>zx3-|N.|<'ndk;-{>/&ɂ0`~]y]ml%ŕ3"1^Ad& ZȗGSV~~.)YGH~DᏌ#;pYᣨ2u?,_>|:)x1Cl/9888888888&4o4I3 1sPW}9NO?8Gvppppq:gv}rB?d4Nܩq)4RFń$ڧ4J8i>~xTi{^Y<7@so_[?FL=z8O~.t\$;8888.N`q꟣3BLfJCiL*C:8Mб΅fr A(9WK]4@g;*1Yd k`I6_b+&h\ch3Xf*99aUAV+Du f*ӬdPB!ΰa79"u$(=h*(e2e$[g'6pɧ+,S[cLJ`^lm7QexFhɁ]&SY{Cnx%K brUҲ $6XFj鬒]Hfb&xUvh&SZ2n7Ψ6 IdER.VW\~$d[Eܷ,1TB"ٲdcC ڷ!E6R)^ug IWW(bP/댪+d;V5PHT${vLͲ)C:JQIq}2 E+L=,лFAl'udF#"IetWZ^%+ؖ͂%&UI}o1sIViM{;888ӴW.H* L1TQ<Vy`S *I!EJ BRZj$T>\>UOWzQbA&Q0r-6U(\}W^n eKzgbBmQPkR*w7lYxdI{l:/]]ul)89Z@ۣЯ=͕۬ddtFzV `h#!WqUYWR'C5R*@G[^Y*_()^fwEN#\sxxB~5%4F!x=u5{m۹xyFZ׺^_Qy_mp*rʵk$Y̶q7ynFjvr񗮂[ Cy@=<}B bs@f5mْ#mjUYjC [ j*T>KZh4 M {(!p[ФƘLHkSqUn'@DB*$>A2 W!) xx1 6ʼnO]aT)lb{vlVN~ 0cGY뮃 GJLu|xJTDLHc3Ud0=lYo0WC;ңldudth`ƸO?ޡjnPHʺwّY]1ŀ~asAP=U壝&OR.:uA lqOp1`S/ 5A+O&T 3X%=T}~~^J&o悔sjK~+JhR7#-V~`E#:̚'iRo0xweH#NS4\bRP9ksE0}0Vghɿ\Y%km}vT`P/A EJ+MAڬ- O1X  "B]z)$b $j@j)$WX,˭l,;hv!U]J$ ,& <#JtڑBi9jQ!ɫ4?IWVh]K{+$Vְ~JݢG0^ѷh o'5:>,(gDDzi[BE 'h8^N>Ꮛ#;88aغc7;valvKQq E%H$q(M5q4kW Ÿ r_iZqˀ]U q!n@%fXfWY2& 喠TmjuH,;$&{Qm0$>G *VXRޤJڣ;ll|&CѻbО:7_sSS VUg "40-y}o2).KH|k"#Ѫujdg8~~jPP5bH֐46 cT2,cQ~Hgp#%Jrź@W=͘8- %nJ|:SIE*D<  eևV +~c.ctHE{?KoF٪dl,C/nɸ(dοdy <#RLJ@ќ7F&4=GGtpGvpp]S\RʲUXrKWT[.arfxů)M\-(V'vKK$1o!WyG_Z|rnJ&WT,ZR` jG Bv"HT vd`V|DCOOw  G!Az$G![r]Sq c5-TV-P:6oZ~Ό$\/Ш' rS^+`)|Q֞<ғ_rMT#3>FT _%0{8?͍QRH~8A~,X'ӓ^ge5 hٱ,Y zBEuAAVcb3NsC"c I*h_N1~ry#,PdiP媾 ܡ:< l7YRBvq۰4.'Pwƕ4}ZtMDkM^ST: E2P[ 6+[lZts1$5V5AZ=۾@-;+īS&@kX,#le~+NɦiW \TUPJgdvh ;F6TXvU~H؀*JhnuRi|{, b'JV ̶9>%A Qi dQ-'4[A(S%|AQHYleNO|mObyd\VlY4@#M7zD`Юܢ=5۟K<<Y9ĶIh7.$Guh~w)i3B+~Uڴ[쒋HN)":3HK]v=&+J+y~r`HХdI-ٹdF18bd+?t.XV36C+M>q{^ḘHH(J^EQ]Uۺ (HPt[ Bs9?Mr(kygΜyϙ[>PHi[b&<yfDWyx`N񱶣6th9Únf{m< 8 /u@" xMDFC .t TmĄ*u1U'5B#CJ$55gI$Ʌ$##[Dt{(=êuu7=9z$ȏ(SPOPUg?xgQM!T/m)D(|;U^ }4M!T9kF#8ҪJ!`M orWj69DбfeQ󏫦eFPt 1(`1// vѡ -( Q!*WPjNYa6JMf x]oQBT eBl &j+Wt`t<(5`V|ҀJ5A滦P_ؽzm> F(=f5M8wBZ)` /gKP+N|8 HJ:s P+3LysyN$3C=๞eI12_N[S9A]g&{x?bQ1|}#)%dD"q(= S`U0kg\s``/H(8znV. |<vڼn dC`洳U/0UT"s.~_`\$UoӂС}AAkLU^ / A’L6̡ v fZGzdg\Zⷬ˶U?o=тpANmE%|w-~%3bnH׶\]Y]؀.i( gkiQpZGֵ *-H~Þy ~[wyá`)8 q {|oD"Z1j- j F9AHma~IH!|WFƝ^cAE"t!B$'2Z"\:S=Iw. z {G$ZU$T8uJg_?Фʠe4e !O||n.p[:,h[}))`nϋO1b6Tl<PLRJ._X"\\?ϻUkFW^S٧.)quq䂞C"H.QSTh %͊DEYSJ6bnH.CDrIST\euS|q8],^_D"H$G `DrIlye{~D"\nD{}ʨH$gD `DrI~$DrEړH.3H$,~:KtPt 1ϽD(N8(tc D]lS$D"H. D"d9t.W->]4ŌY9s>rs1Қԉ-%T%-b[!H$Dra`Drrj/PQ#z7@IX0`_h"H$D"`Drr8=+1A oγrN*z;cs1UBOzymۢһ ZƂM>V !m =OcF]G642$`gĭ:UWiac&ni 6e ^vI[Gu[},=XXU@T#; YWg#|l+H$Dr `OilM,77Q%(k=fsm%~fy$>E秊,f3- M1PhՉ-i˹1*#ÎCExf:5kb_wxw%ŀثh iL/|̇ 8X:c_ z d' B:ѡ5t\F]%|i9E1 {4ƌ2G7mр͍tU<(8Q$pY wӰ%xnfQbD"H$>\FK'T@AG lb> t,阨0-Gxaa:}f̊| 6cnЅB8_>-ݰ w uTa'X"H$eU.>ҋt]Ů.2pHi&_TeAIT67h>o^.e=/ο{H tϖ6Sxp,P.+N|pvvL|zc2+PJƱZvK.m yMkT|5gc ҧo[-e]n Q{6Fug`V8? w.eʪzG|ߟI.Q"p:wi6)z(n¬@ *D‡v~j%ڶp/NwP^-`3~-(,m}1K^,h.$Qlj L8ثL+-h#'p` Jp46 JcL K+ρDr);)H$D9cu\E*oo}w>]׺,;O &e@!zmqnj(I_ܕԯ[ ;6yu^ "y_QyxniZ#}'d2pYClWweZ~j6aOA73nHk]Lh!cj\-a[I]L?7`ķ?4y8Ѭ-<:3 ug|yj<]kk>;Y'tNw;^iv~;ڕ;U~hA[gNc+UEދ9m7m30- i8k6Wբ:eGjBaG},x9e>L\G| ˃N1%-al.h^|EEtki/=ߝ"*%1تw6$ $t?Xi='ν="ߕݬ\Rr߀!W`.2q84J{?-/Q}آCP !DGGAPX+&3I$ҲY_\@|뼻5Pص }̼4nq_)I^`DŇEw"*>wR(4;.I&a 15P@8`(բrM7Mu27bldo{42LN aV>~5YgF^#uV,D;<$Dr.ԩ-XëW7ѹE}&.](gʩ5OntaN}hUݝ<R/6'u]g07ׅsײ;r{cĝDaF\{8z}U 'פwxo Lǿãrwig{QȨ@¨~C3ƏykN2vt{̝Ω}ȎhJF~{HyE.3c 86yMbu-|ك\Mc]xi~oo(J!e򷉟qoӊ<"g/#?fL>4c^囝zJYrw[~wG:[tҿ[_aDCmmMۺdKxd^˿WѰf|ӱaWڜDɉZWwtonpkr2[ 㦦Fީa3ʴoG`(oN=3i>N-㋜ft8ږb]PV}{g>#-+*RI/^U:&t7WVhڗyx5T#:(t IDATA(xq^lI6G/05V?6.X Yg,lH$ŠN;/<͜auXrS0MU3f׫GZ1|kJeu-y?+Y{=#έ7= SAE'UL?fW>XԊ`5&1Ev|{C@b57ilk oAC57Ȉ_mހ_fTIDjT#R"܍haEE^G>0}: OlMe[@$R񇾵_g f,Ft?(zbO(ZG=h3 B`;p{64r^,TX{qV"ϽuSUnH=jRZ-\7BLj>+"0x==w<͘ Q!u J|nݰ.Ͽt2=D!BB r7Atl[JP{Nfzjx)|W֫/#sV)p`lH$š"קv&#T8-O'K _о_EXx߸`_옏Aud58`gA1R[YDDD #pnN]XdBDB*Vt\y?.J3vb yAQ譩8]^Ty :&DV?%z!8RAL%&T=Dt2EAQ DTeV©<* mn%$7(C>g.#ᑡ~@Du,]:b:MA)|/(,~w@ZjSﳭigEPR*(iOCת{qNAnoyUzVC:}qM"$H_mD"HJ:Խ^ xkF{s7(Waa,\jBECr8ø9'Y9 Cy/uםiLfaصNAAw78hq8/d&,Ԅ3Wj&ty5|ƢVR !j]TV1 %6n-uQy^JK=,Vjzl}Kt {!њHaLݵ#+qW,ץ%q >1PM>o9O?[?96ڄif=9WqJ{ҙIQVMg+y i.[_jc/Ed)~-.KD"4dZ&*ʺZ)pjl?ޣzͮ65⇴2?s:3w5;nggaC3v&5tp0=Hȫ3f恖qF:>R.Ks˽wUEhO|<:*0KyOކ{68̬-iq]́%`CI5lnw3߅]'v6.6qrd{>nbG c ;G7x|&K)-)u|:NטCHHhd:K)qUlA 3,$eg4g?]\ǘ-ݹOr7ѹ`KV(Q鬃8s&KOF~/<Óײ'?ݹ=$/اFѱ[s3n&#g.a7Y([`<.Jg؂Z6 s)sQ<\S~IF&5'<6Sw^~&@)߻7NoN!ʦ3bA}%` 2xP_"#jn- D"ς_Q%H$ $J$o_XNQ{~@#oyAPiۊՠtbi%?o1O}㣘n 孠fL)&F/7yQєPRi0c?w}X[.ό_M[EѺ']×bC&7O`bӜp՟{y>>^˟=ĨQTJyq HHcUJ{7nO^?P֗Juws/)O_@` Dl׾xiH=t%X5RZ\ a6AbT"i芡r_KIA K8ѡ5U=gkt,g>Gm.C(*UkB1bz2@5 tͿTMO(5ϳ"X* TsEQ|j=x'GsE!`jTކ]S;^'%n DXTj8 )%H+Ƴ̥WO(:#\-zeE|%䔘KVg222EDQoτaպ:L ۃI|l9+iU"X"\0N p]DrQ|bWŹ: ^W .a~ᬗA  Ï%1+() kKM?#%?a#81~&בRB?~Ǐ8^_Ęp|p~5.u-g0al޺NJmo( +8.EDrpEc&rJH,H._|_ɜJWg E8ˋby|įURH X c :*:ǻGϥ N OtKk\Ȗ-[這NR:ے@3T9?-ix.뵧R~ocfD_fC]RTTRkz1ё$7p~D]={Q6HK,\p[)~kvYSHD"ԉJb-`za_@m˶<:~oo{e/c%jD"ms!ͣ"t:# Oa6(WPT.%1$s@ :2s^y܀ {|49'o?;҇ctZoÁ|zsppȦFGv:[`Oι9'X19?{(;lf ,~Lx3Cufd3!<ݶdoAՁܧuρJveז8Qyn`χ8akiw֒S_Jox䍥z>#^Pk%H$D"\$f dIIe#vCy5RaB;Kh;!]ӡь[PDnsЫ)7Fzbvo@)JE:@z Gɣ4aX8 j2VQIё9s:?ݜF8sk]P/C1[擽^0,gVUo CS[Ⱦ]Зц{+P8Ҁ=o_owM},vr$һt1pX[j2x/[v6yh1-XA;3Z[籵L{ [h(hh֫Rqo>V$b*Xʹo?0, : ~CH$犞Ȣ} p;r38ꉢEbD?z]d4nޔԈj:ٛAJ&4 z 9GsഇmNMO_Qr7Ćb9sHcTCaKgjbt cNo{?y;qno+0tdNO)5p9]!}9nBPnCIng6yȚ쌿D"^7mHN榃x~l;"k[蟔LŬ4o7ڛSriPR:JpQbwzQ-!. BX݋9ԥa 7ʏPauݖ\x2Ӵ=gv!kxܼOHr( `Qϧ,%=,b▔*OO6+ bb+Fd lv:NӬ?081!USosSZނs's3Sjњc]|=s5-灆ҩH$20{=|n .ܕ&1!%l=/A!ΊB)5_ 1=mcsG|vWܠ 5va #;Lk*/~gn뾅mɉn}i kOM֝А4Ή;)Hn9ٔK|2e1벽 .Md\,&7ynwI3an);&b77bj5ݦ2$,x ď{[}?ξ@#Z.~sl<Lne¤\$P4m?'o@}'%G6+j3g茺q{bg SA@7]wQ́#$vL%NPւ w?)qrP`q69h\񿊅pMj1m92{1v/&eE넶{ZmG݋ykVNj B#cX8 >\N0x}ޤu7S0"YaJQX6 .捯- mE-_phF+6W)t ʇ"k0](|n _fA?ׅӫ`Z0>ȆcF [s;q&lVj]x].\~3b Btkxq3x 4[41ai0'omE$:fLFUvz0X- q5'Qd׼rA-0elbqF}Fs@ĿXͺ* c(m E`l؇'Ѕޓ,=%\*ktC|^4@TR)א^1*i)Լ5v0 U &pUJ\ga~\62m8{5_V+4:m?ȺtAѩl)),>|<* vs4%8'zmD%V`ES'@|>`(_MaevjUXhgUA=WըS*<4#QĺBXR{;iZѩgΒV fW|3cS]fbB,D+bl:&:Rã؋x2m"<ag|!Cd4q6| 3dX—,ᛏejtsRn0X[i^/5 $,b<&*O&Ng碨a4O2/#Gfj&c}䗴`&T|7)@(-%qn4(>>=ursQ %*8mG4Ү@Po+,P ]"4J+F6'`&cGZlX֓fWq=znNJt8>#-!ѬFvrNF{:5#ZM z*6=B)ɥX18')" OZмx4kplbT5|>BfHRD ۾_<.ph(Co_`P}:T4kZB\ݳfXլ5-fɮ4jL"Z%Az#H$+B j@/H.y~\FĻ#Zֿi;nʵFQ!>5襇؞ӏ }($ƣ*56nB{c$PQV6޸)Pi^ eɬIJfߵ[ 46sw]ڽ5`ٰj G^1x8yθf'JQDE QnZV[u=Qp(Ȑ yHB!z{=9瞜{>Mxq+^.G$ڋ=MCy/"D=", jԶh^\ˉtmslz?z͜|ͯ`1'u)ǟVϜwGߤg gXLia[k%S8_L9X8Io+l Z\)'vo(oFD;I[$R.V!<6ɬ DnSWd;:U^ʑy%i:7ڜ׸{*-Oz1h_7Cs72%T;؉jVV%T߇7RY1#53g 346X;o6,\&'sDV>x1 zF[KMaN=3M)tIb4м wnb{sِ쳛gŦƝ>^}Z9/~Gv=j=OGL<Wj+7İ%3QY\u\2ח7e>X u.\[̴9HCuyr2zcf56kgL6Y}gE!.\{+S_KR/?`~śM,?V4>34:qeZ'w%`i㻟[CDDDVF$21|v& QӢ1IАC>tLҔ1M'I7?A"°q*O.B|9I&ߺ5l%nreœ^Mݹ|±t/ZKl~5QY+)ȏ+q{ѷyvYQձrwQe_xuyn~%5in`^ 'R݊/㡿wƬ= i A=nZ]8 {e&o}EϾx-˾+(>be'a MN3 ?<CO9^_)gɵ/|5\K>t}Hz qg/}mʉ=^8{^}'@HasdIc9W/b`K?θu.y}0o*>_e-~ސ\I;@s(L3s\ao=Ɵ+]LSø撡tߴSVҧO5iS_lߖgv['p4L}_63:Rn+Q tL\\ӯ]rJc)=({?MBG1|ő߿0]շtS;10Iq~i[;exOrj0y sp\Ӧ#+C/1ejܝ]b'jL)˷֔ggx%t>wt9 _EmE_+f-dc34BўywFPJDXj6[ٔpDxi6 _#'3Ѭ&9D0'LQ%l6ΝR *kikpRoHa0ӳ E*naD^Ә={ǎJTY:o5u{&r1x:N]̐n :eE}ҧO!o͜#($X4k9x:r gpŒ/LeW.7'A 0~{Re>}R~^=ԗxwAndݾKQ}c[Qomy<<?YpOٗ>LQ<1G'm^E6$mj>|gؔ`vJdF$ H8lJEŶc1d[Ωkg0=4i%S^h[.;MUXr~&K` ?p鑎ň ;Csm|HT"AZUX m3̶sE^s6v5eL=e-$u)#єDo>^:\uk\|a %.qd>^޻cjˀ0_ΣK \{ ^y5K`b}ٌ휩KnG^Ʋ< =b.DVB$^];^ajA ^q"Z^{->ri)٦7>ei <#ŃOoݗ_akwY#қs15*02pt܆$Wh :w-MW&?Gg31xiE4У3a Cc ){w[]}tݹ&?g}?NgP;qܗܮ|`4)YC_?W^\XQu3@ݏF/7rCǗpߞ1'u+'\}P\= ̯l݌fvǶIƕjwPT KԐ2(/ $4a?_3nk',|8~ௌ 7{3oQ^wj"SfTE<҉8  klj&]GȹpvCN)md\ B e*bF(tHĒ8Y;NƉ&|)#jI'Y:VAv&OCD}@[TL(c lw.HAibsM"鑌!|X!0fIјk?Ub׷URQݽ_QFxO(oq-"24?)گp<}KY^y\pER\ ?~|%f9 ?*ns pXaJq)fgZ3%~|e޷#$?ܾx H&ŌNRxQQe/iCVsskI.4 "gW7hB';5`$A(vl@f]V`$N^yK$ݑ(K7vnwC. @{lvFw7Bw[D[{wWerQKx唍ij]L5%4,㽅>^w% t|0N@:DǏoN% J+Ji;'uly3QPљTH ) ezI'M,aSQ ZۿDQ">z@((eiUf|$|lb`-/^WVs(8IAV4ID8J˶$v~Vࣕ'үo R-e| q\cikLd6IppzN /? 'Kg2? ]O,j<5';0d%/.>ߒƲ$^#3_y>@q1ԇe*?)߈+K?OZ< ,Gayj\~|۹wl18x^d`E9 \ t1/߅qWt@%]Qe_yYd>XYK3tfc}^ %cyX6q=IəǗS^"\Wط/YH*cKAw-jߥKgt fRswV?DziV̯ko)Op/8EysQ _q5w lÿ}>Of˴ypY'n ج|QD梣gSԑUrۢٵ *߃I| 26Iv%%#bqQd"}1QwK(_w&UEQՍ'\ʨx>"=7tZúKYS+/ҜS85⒗î[˔)UcS^}!|4eDOg5P~r޾]M8`˦*EYUI|xwD ?Pܣ|e΄Չ4>jJ!RQЇ=^xc|KpLgLAiʺF0ʺP8e-Kv!'KHs`69t^}k6ycROA:%)YeGiCCU((\[3|l#Y.$9nHozͳe:h..<˟KitǘۖͰN0v0淳B!0™|cV7Lz"pE|v=a_Xq uƦz(4(qëj}\pE_mZ˛O+eSBhΫv꺎vYXQEQE9x-z<nqQ"`hH\Ǣzs㙋Xj:k .KDTw`N65̯:Ҏ&«gÈR :mj`1c\ 'C~;^e%;e7Kz38$[΀3{Kx5>}{d>^} eALtTgre<>x6(#4ڝMZ>h)C*rykQz{|Y*VEQEQ/f{g6>.?NѬ@7vȎ=yxޙ #k{$P>+zjDe͔f\鶻h9'ObQ@N}-?ĖAZ : Bsceyd!cẚgyQ=f~`ө6bNpH򅄔rwEQuJ((_:\b gVp8?ީ<5餇܏Ӹ3HNNW0a`"`Z=6q?E_f47kƎXA(+]pЏEEQꊦ(r4TEQi`]XQC EQ۳(3 7W(אj PE9M~~Uz,Xc..IZrÂHXFώ_#3q+(US(!LܐL_2}.I{?PQ*eCe^xc&yUЄ:{ /@4_Kl=l VФ`EQCi<%.9L_R)򿔴.,vC#z{i`iKՄ)`EQCؾ[$L8%iE9L_2oǼ:mRݻ` ES(!u|޵xz{v'M0@j% Э\3[&,' W J(%yiCC&mУÞ:㸪g(_O*VE9DWcWVyխolfI$u<)$RJtk3$_T$#5=VQF-rhLJeq,5+`EQCy&ӜPj9M,@ӷԙux yHI0`r1<N"_IQkj;tڢA+? ]ORKkBڍ 4gy@!l @\Qe_wk|fS,#nmۺ$y6R:lX $ uV<rs~B9v-NQE!=뭱7EQWaz^U[mVY:9)SKi!>@sp Q4_. BEh($YkN&s,_Z˛]l֮^N(7,_ /Sܼ\r :֒[RB2Gr08e@gwCEQ`³%%B iA('-դy `#AH #ĉmXt芢(eWluȷn~5MC̏$c zv)##zS0_O33zucN>=J֥p0D;2-GReU.*] EQ̗=5$'8_#+U9du-[k(P&%X5h =@fӣ[7 ]蔏G^|e5*x=wX\DD{1^VSVl WҝAQPݍEٿWy$[ġg IoF6x#Q _$א[N5)--K\VJSCa Cɥ+HT">'Ycb9}\`Li8[ >}9$7?lϜtRۿ~Lb'`0|Ӵ~C~󍡷ES([Zf4 !m ûw!/ ?N[Xqtb*znpeT @jee\fHT瘁.O~FBVW%h`.5C$[jxiIekS^@]k)#4%8-"M H[i]TJM, RҘ/j6|vi b)-[@Pw$ ͽ4Cx2l ܴ$Ѣ0BM+2LKڴk okbP7YV``&\AHHR;>=Wclُq|FQ{kLymS@@$M(oԐRϑkg/^.&ZK獰D!ʮܢ6^ϒ󩞱+ټu 6N!`q!G90x@t|(f `=W3oV`7 ;ӹs'6Du5JKEUHذy+=rx$5tbcjnXGk_lq.Tphޗ>۲jZfI1MQ\Y߾DztAG$+L"|n6#k|د%s w'%&$6|)5R0?@w<^x9̓kw:qIeOZ|h`@~+5ɧS>_g0řL߲G>)5`G)nDr~~8PC@$]~!s3)t@$3?HIq~t4ؠ>]2J}ߐlcq. qTMA  SO> 1ibK ۣ4y//Ns.&t ÝOZL1x&FR.0GG&ؤ^f`t{#g Y5M-[Cx@˿OVMT.0eWNt>~;ƠRT.m>9>Ck`me4c;LymKЗ I?͖kشb-sqSCN9dƎ;aG>л(J;9^^b{sdEJI?q0EYY 5T(,l|ѫ;y7HZ37nFv~.p\x`}e%=/9l@ rCӾʄX-?Ј39>y.{qּW#Ng~5̘2o?"lC#C~-\GܦsͷUsN gK2mjS\zXiGGn!qOOgt]u@p[q!T6onf+(8RG>~?Њյ - ڜ2zo|<^Dr:ubͺuLuk6Pѳ;C #s LuR[5;m=s|?=]̦ܛ^NV?"&WbrIi'DDt{_L+g[Mun:M'6-O=~. -o)]u& T70ðփ>~1P ީNC yX|n-͜*cj\u f2ۡi 9yi Bp>i)u 7 u6J$͹xJ1=.3,$ׯJM:DŽ)[_?/$t矣Lèe;Ia̠FeU!C'S$[&9ܹz;3_V72]ۆu͂[טp 5>bZyTiO=fr<` JkD;6K&w\lr^'A>w?R,Npظ}7O$+ h&?$-ȠUXxH RӜҹfq$Sj{m8l?>S?MSU%E2D@" W1Fut0\bԮZCyx We]BQKOc YГAt&ǗU#\,nL`').,XHͱGwaMDsALx1v:!5RZLcCc6˟O^ M <6Y2(ڛxS9qOc9;s#'5'om+8 =;ǎ֑YݤfpmWȝ΂W3x}ܽz6wվh$-SQxx'r^6/vIqwSױ|^}"z<>?_[C3||L^A~Ez9mхdӯN6:lzuN}m-O6 u+)$ ѭ q'==,2͓u %2bbFIY5-1JS݅ۊtsL xgyH4NaxçՒ}[#fѽ*>yYвcf &lq&%&W%oKe󍎒ɏ9NHg6hT- ie:6M= z;<(Z$, tP Xan2nZV3( àzâmњ7I< U2ޠW4SpLO96֬y: x5s?6yb?Tׂ/(05^ʝ+]bٜ)orFYKzG*[и#OOacAʒL"8:e]&]^#uO#)Ks=NQ{M陸̽27)ieYer kHhAXlt]|A*x".[<k4۷`SziL glDƋi uǧ1%i&sVaQ(g|^Fu$GSW r0}&Rpe]K LtpW'O"NVHi)a|V&OdHE_4=/o+3ku=im]wkYb7Mߤe֧+osfEq>3;=5kg"#E̡~cEjOA&6ztv6,eadt4RRF9& P|IkǝecʭZu=s5R)ǧ)|"c5r+Cj wh~Xd>{/[cx-`y& AcLvb 55I K،?$%֎q)|w/7^ãoڼVyaAnOKv,.bsɭ8/%%ND; -nzQ>:ε.AlE`QҴ}ܩ ?$g=ZMA4f gTiE];*0ZWJ.74qM}q9E 7uM dN@?20mq$V]܃hܸ$֢(X~A`K6Q/ FnyIqϯOY y^H ](CZ5n)Wx.kGZhB0EtF>@(K,=rp?1ӭc!i J}͗ە6Ѥ4$Q#ƂՕt-WN_ơMK֕2,p&O8t%Hp"#M26dM5F5BH {_'I\+">V#onO]9#bTդt'Nnq>d-[Ϫ:sauw>=Zb$ dh# !aE9vCz:JMb#A~a9$ZbiI}h iijbt,(Bm.|&$̴, !pWeDCMgcBa:@Kz4mDKDn;&9&@ D^'MA [;)^1r[1YMCRXcsDCy[7}Ccs)^+N=?],eVD;rSܴX$)ɖ%oB~_m{9UI k?ON\nr%)n (@bܩ;\?Zc)X&pVkw[}HӒuTM ÙdeoE~ˣ!sϳmAg؅|rG AE5>ӂ12Fq#9}Q%XQ='k5y^GJߏ>]Kf_?򾥜3f8b]L^X" n=/ğ7zx"AC2zp)ZbV5?nCI)Z{n"gf0u=Ϝɯ0gRϻo՟4wy=b}9f>uʟǟo^ڸW]oG 1g\ŏFÛw?G޴a'p0I1 -!b4۴4M1!=֣W@^=@-+>ˊr-%((g#{77(:Sҹ+zSY`ʆZ];V !H{WeZ?, Av$to227BaXek3ZHg@ \6L똛ѿydJ`tGݦM.Kt3jWcW3ĠFI!GmaAEn1sGM2oGɸR|,;htj[*BJSbS;-Լ*+%DǴju:mEs9H>["Wa IDAT)d20s<&(Q.s .Ȍ53mchܐqOVKݷz,th2jOVfm?* N3%uNwme;B]gCJN]&9䄜Bk78*aA4*cV85׭k1Hc2Un6ʴ~ 5x˛ۘpRU?gbx&bPH(ٺg.3KLNɢ C>H8FvX` KF$03pz wwf==s1GfA@ y]6ٝԡ~,9E?ãtWW 3vUoVH-S#k+E[,wN3tw sW nw$?ԉ/iKقf4(ټK㸫.OqkCnHKf4NEuǑE5 b)ཹ+ɖe|*Aֈ<7㏥CJNI-6n;ɂ߻?$̚5 [Ӱ1nZL" ][qϮ=w_Sxm%Yd@7N Vmq$Oe>nAQlƾg} *'{ /W&L]ae`a˩7<'xwĴ%Yle)z b:Q "%ߕo~^ᔳ\ܫ70sR 0%;ſ| v/'xn>Bɸ7_exd6v1CJ*jBbjQk6 M0;ū7 D/#<憋\gT(56ULu6 @j͛oYQ#kpPYFa%# @X;(ilO:"e7e^b.^?~`nD㶳m27}ћi\s6ϼm4IW_IPn|z'C.n?-* \3a^2gx]nN맙ý|Dj5In֔L`UT{xRF]Y,`OqalDS# WxR].u uG$FE&oɟOD1LTqJ\R-YʬuqR"&9z| $-@Mwxn9N}^w}AHi|km5lu+ eI.ZidL-$w1fT)P@2$U`Gmr&~B$M[)YmziNo&ĘeM}gS߷- I6aU&>o_Mo6q.cI}g$A설.IZ$%6yc}"&z66WM}ؼ4}7M.mz--Sq&})N\IQe*ƘES 5J l>i[1&^5܎-86~AT@Nsٶm^enz~qI.THrhhl]"JRR~_U|5>Ӻ $$U{p5s>/<6ѩG*갬:T=i#mAt#BQ6H|#d$_%7O*J*a(Q!i0m<Ͽ1M_Dqq,L`<tv-9Dir^F!]U둥̨!ŧӳs1dHAM d2Dtl)iґG^k/6 w{ {<|!8;9z:bz4K.}%_̧)mqO>䟷=NzU߃<0\ _aYZO-խT4k*.u/עǖп7?4D(WN#$B%0[$ARI2rQl包ElysP]*mj ݍ!6]d&ţrkgmiJi+c{$ogi5i txfɸܲx6lhjS_wW:6%U[3l7%$ۑeIHl-vlVvj׾r7ɤ#bAv<ަ{`kskH*OېT츝{OUso?ehBG.PtVE4RAQ$t4%Lӂpk/rT/!ZGQrbac&R^g$ܳF݇]DT_ze)y$UhEJ0hux[5ؔZd0L(^+.D4M aqa[]J>G0<.;QY3CrtQRtFkWCژDQ]f(%6EiH >mdO(L@l,[”E6I4SohܻDHRXeZjSTжӲ;lp썢"|ԟб2R?M5rP0f}D\V6Rn*Zxl?GŅ%n1 t_ Pl~f2,l4MlۦZLq  xe=:}.uڟqGDkhժ> 9 Yj:G#[[EGwKLSV*ޔ0.ĺ뤝CdF4bl!iؕQU62YDv զJP%m2g:/q[|ӊz.b,Ç*@uiyQ-?x*4g'+NؓsrlV&ۅ 4[5Okk`N4PEIm{ϓr؆!7bs1=]c~!Fzh@dv()~7_Uw(*諂;Bf`j;U9}.u8_۫ou F҇MM`EQpY:B aSЮ!͈:5~A*61=QNp׽a'_^v*8%kEYDAn4RlٌvGism-F˝ۊr@ NCtKG)xn|~?a]%E%UkRB覅"([Y9 I,6nFe̛ =1!pqq,BҲJpΑ]Q()# uAu,ѹ lB(tڍf9a*-@ J-U;($Ui m1[&M5-,[aq8?Wp| k띭ъ"ї-Yݚ o Nma$ hHiiђںevnb1ރu^Tt %r4vp=;!X/۸j,%g7nlɧdѡUESq\\L2i$ 4v tp78sHJ' sI9aAP@aќ5yקr| ;B=Y7sYEEW V.-.;Df @QU]zuαFGP> @(iC3j "4Gq'6,E ~+N!yEQģΝz߫p8#;cQRхU`""\ aOh}ahY$ ;UtMEm#N_@$<6ɽpVCN/ M70Xfwff(+d$4) %T[`HÅPuTMi}:=ޕN'%MiFJtI*`{q+A}M 5Qsj"V_Ce]s'yη'pe؛ ^7{or Mضa*#Il@Z>Nt8=Nf9*?}F6-Z(r[ТFMc*QAB";7DR_M1iDj2cc HP\mXy6y'RD RG ΌQ[V='?[Ic 2[wG+J'=•}Ɋfư7sbXnzh]ÍM޾Y-Nm}ƲuMr_<<;RH}*'aД8CpňteS Þ'AOy}xvI@֠*Fݷ kP]\]jkIicU,[Gy{s 㠗=@O#@ޢf ̢O2VqG B&$а,fx}!|aT9,bu$NpVS9{FfYYISqn)tlt0}[%BޡiB3S 6[T3MXEIƆ%F4?hⶁnK7l؋8y~DMo\B7ysySֳ'rӣox| Wz¦,J3=$_ә8MGzD dKxm#M*-GwmsSVB$K)+7YO4 uKד߽ 3=,n PllKÖ:%%qTo,!--k^8RWl7lX^nDLiO.AK lOECM\gR}k3e 64Duܹ%m$.!)h`rR|E~R3HByu5U`@ D"ϔuAEIa56"[USmV`ɽp ?ne7Ҡ4]:ꨤynh}yf,c5-2NO Jۡ'qsKw[v8=x@(H,'cWձlR: !P$"i򐤎xa! љ= /~G,)݇$춸AddJAQM.&aɘѩK6'KeP%L6YY0- M9֯8.K:a|"zSӴ t?n:߆W*g ʑͲgSrզ\6*XRR@x;i ,=DېJlVbVhR\hQ2D=#EQG1Rrd{_i v+@5_N8Ilu/ygnvl h^\Eړ0_:/L`@آϙZ ; {R~r{sPsF>mXQl= ضK*$q gم:=ne%}#+~˯GzRAQ6PQ8EE=?eZ*ۖV%fEAQlG 6aWQC[t29^úkhfUrFIM$o~AOЮBlY 9 $H&LQ4>q*XTkSS*!E'VP P\H ztͣPf" B6uv$ 9ߞkvim\pdEHNUB@l8zHkøn|3^؊ڻk1M6CosP%Koav =;ϙAR"jʘ =JZz;Qƕvȵ~JK A}M=%_H%=? 2>zv,]5STT/z񇖑<]p8P}?Z_CVSoNAA1'!kWA!Wf'S45 ތ\[4L{ʂ *I!AbsNم؟S\ֳAY!#r ZOG*/w;mDjjiC4 H`(n*$*&-=n&H m߲"0g>6bGh45)\;FU)ak[(<7t/lF`͚RHFt -Zt&\@]"<6j0O>6u (hJgS[<g%0pb,\4[reNBo]Kx_> .jm*1-O>$ GGUYkla6=7tnڵz>2\igƣ4Z~?,lEE4Lt],t;$8qQ)Op8yyy=,B I@#h Kefxu¤ K&QZZq};ɢ1Abc V#Mb1U2{(:Ll'p n}n"tByV^Rҽ~8Iş8y3 7?&C͐D!EIq^b IDATr< 7]w "XB kҶc{Wi[hu1ymц 6iD ֣l8Yoû+adಋO_Bbt}y*+4ܭ8~}I"_z)2Q_w../Zȸc* ?tNiKYv1sg }W^02H^Y{;p17_H )-aU`).|4ҽ ja׼s%s]ȥTS$^tdFK%Ҭ#=Ч4Mw8~)x/*˫Oqq!QQQl)-{2suQ#$}>Rj=ZoÝ^J"J `r:wU mj222mRk,|r۝Zk`ؗyt=+&Ϧ<79.iP[وo#H<sv%?K#?7#>MO{_;~>Kh;w't3GAn^ĂM* ~35uA'u8جC–@THhn$!?æN8J/"RnPGj@L& WL7Z{}vqCEuC1.Ub ZC*&h~`3j+,^]@iiiz01:TJk馛!BGA (Ba\ (FuE-◬kld(mDTST%M.bÜL\VITdg 1@UU!ٓ^)>-tsZf Et \;CFoKVpI%YnTEŗJ؉p8srcBDƬ#$H#F=UvV ˱ޯ~u@QB#QlBp•'wMjv9wj߹ '2Uթ.!}2-F#UL-GK+gcayEL`IICdC$#% Mu@CQ4BHo׋Nۃսl.av8` COSqL75c/'ԛ:.H6$.5֠ c&b +F]tSp8A +++>:fXhH)VmB8F0 @Ud`\%"E(U$&!huD6FJ^$mu8~uN 2Y\!޸J"l a"MA06k\m۸TTViGM#†d  &ڨGvgWTtt w <w*ks iqj"ljbA6h&XHV6ı 8Ap8Ǐ2tx3`j BZ5-RP1QSOL|HW LN1iP$UIz[7 iqOp@oi >Eçh|BcKB`)Il5  &Q?jFǑtϯP՜~]}v L$ (JcCQ% iީ3}& jPS۳ 褲dRJMf1hEv~fŬI4U0o6z"nSX?kskL$d'Yp8Ǯ;t rN9u0S4,ʬPIT(E4R$uZr;FePN.#39|8'[t%j˾CuM¶F*ĖebR$IHF(MPTt{89]#%#gwwg[bD7]O83Fj*'sy*/-7xT̄MJaԖ;<ɕ> SYXnKt"e3ܳlmZ0q;̀ePykS./Ϩ`p8cz{ӥx^~3K04XTxL7Bl4L2pUIm:ժK:pr8R_lXpcQW-` ֬DJ ITE"@Q-yXEN뎝iV0-PuŭD#u1#V73#~$1M7|nb4lMGx#HP 6k2WY=*n墾@ b~abK~?~ݩ(**J=CHf8RemdЪUԛ_ƬKl>?520 k4@E46tق.v]%fdúTl\OEI1U7F0uhN CZV Ҳ[ݺ)^{5Qs_;_oysV3o:.|IYGu0A~7fyt;W_.ZuD~9#p2̊D-;5'A-hMlN=sЁJ_v9vñHvALkqᅥu,иcuY]/eYm}ʸﵯV/g?>#Lߜwl ceX1=*>yW=ćeDۿf?]fJ|sYxKCKh)>mRX~Uc`/ ñ35/>U^^pd[<5]|$YtU pldӷ \0~wuuuS"̱|>iGq<;/'h]O<0 ~&/zOa^-m.bxxXX$?? XL}K*Iȋy:)2MyL[ҵږ_8qVQږMw|mK37'LDV{:ekxe^( J`MI0~]{i,o1b[ܿm|^xf,mzW|ޞMUO`(ΦKWɴwcHwx0Zt ?Upk<Rw K|j2oУk[c$˾xwA)LzߜAYҤp|WW tOxUf O=38 e&T.+ %x[wMh#U5e'/}:iydB%;L4{s:#MRK~UF-sK5Zu%% |h^UmgtG L}e~}<:eP1(_0^qZ9ooZC-ƺI6oN^Juݚr20f 9{緽$߀M$wa)3 k%h&x$FKuX?/'i#'8;c728dzgfײq< /a NLll)NN֚M{7,xXC/f84li_Ǹ{IC]%\v#O1]I|~$%"ҩD殯ޕ֍ӹoA|||TNhʭ(>ܸH q+ fMP)0.vѹgG>K%*o-fX;FY>DWMajX KK$@5]GڸRybe^{j-S?O~-$Vo1_cVͥW>ʤ]l$XF1:tkX.9f^Ztj&~9p+ϭ&aG3Gr~'(ApEp @5(@t)_y}TINZǨ.7 HRˊA '%"%Ԃ<1_,NzQ&hCêo wy˸MӨ ,d(f_.-o6w!4xN?bЦ'GQ@0f msǖñ~,}r>㆗^$CcDʙG4t;чpg_//<{{ ܒ@e5|bP:B4R:?{{r]~G 9g(Eǰ\}I~ӗU$(uO#OO"dp}2vRϸpNk5ǒUd]u0rpkHgf᷋(1҅L_>-P703UwQﳅH i_3/p 3GM̟lWfIMKF^y \Nj{s>`fnN ٵ|0< 1 90 ԧ<\ѹkl2/xׯoz^·ev8?[_=oK|v ?$2q%VAޞ?Cn0Ewx`0H_e}a}<'_=<) ^O`ɰ%eIi&B53z`3 Dn-CZp8s`R"ľ. rEg346+%Y?XznYˬ Y\\ImdEuZ))5ZĖ>v;VP){7:3bȦK`aE&k1|;+(=[id"o˓}9>Yk O۹jc81<֣7XԚSE!=e=!b5+ŲEȟ2Mj WE!Ƕeυ /O!5Ӷ~-4RPzΗ3~*{3TuI w8_Np8P|gy!9B3$tME(ГF-(䍛o9jaj݆Rĺm8kG%Kќt$'@4*I6Zp'S{-` /up0{|#z3 }f[K(!> q}_@ItAӜpl=a78)|C&'_g*z?&GǞcF^(sH;XhEbGWp{mn2hnd2maI !] ߅m$8&iH\^Q F,2'<)]Օ/HNtl۽'W۪%iB1j課ՌC9gd;SeaGUREؑ\|>HBV^|2ʏ,~ l'v8cNp8PҖ\_3ZT9oS$fpO` 0~bU7|†>4D6d{VǝՓ o}[#^?v޶;Fz^ՅGg/fQ:^qOusI|R>Cy0#Dxi5,p:x? /ׇm;\;$xθp ow*`RUZMMiHTK!]CFyw% g˷ Y)7!}/de1%ey]'pJ2{?iSju Zo>D8Y=DCⴔRBT{˫*uWT(4!.#HHBR~遲3wܝݽ9OQP4q.UGKFryo<NdF`ﶭ4{Ht=Po ;#jiX. ¿;qaD6|gRVcA.锕y11pڍv =ҾMyv^Q%!V w%< Cqk^13Ϥb K;ȦRL a.+e[$G8!RLqoZ*oS dtl֖ԥ[ M!u|wv2c';Qo3uM@I*ݡlP=ȴ/TZ@:I6xaޞ'Ksk;)=ûXYQck^dKjPF6wcb`VM9}?^ o?|< {s^}bti`F#jΰl:_Z2䜣1Mn' 0M B+KuO/ ޢ},Yn|;KwK|ի;84S wKY'|Lw є( qgP4huӼR8t1$]Gn>B5{o FPsF]{>)UҔdž*(GS{ nf5mI=ygU[1M$hTHE7]Apcl9G߇k;M[.f=AϾ;Vjl1[5 ^?숌ц90ڼ%9:THY۩Wrrw.mwl=ނ-+ r"wO.} ;Wy6/_~zQ+Nj\ꂜ̐kz2n=dN62ua9.>fz |> ǂ  ~~l1Z#,K`ߓ0py2fϩZ[yqDP7cB\]FCFF^z>CJm'Q/0޼3<ݔ>~ EMWu?;4vgJTV/[x7i!VVLLdp,VuȦA&+ MUB]3?lj )yl!&*a!% .a(H~?bZ' Hle]y1.WjK)a$z&[U44?y}ÈrY@a.ȥt-<%QB `(0d&X`T)ʧo?ܹ*vX o9E:A[{MjSˇ~JJ) X !+_+PF^NChUzv/. %3((gZ@ZE䕚pU˓ Et/^ YCU{@XB# je}E>Meysg:c+ȧX !&^|s_q^ghwk.} RdEI4 L]G鈣(-OƟ> IDAT4MJ=;v|r%pD ¿5Kږh.c\5AїTlZ [\av}j]k Y >4ϻb#  ,e *A89Mcgnj|ƾB9eI݊%ST+NzF+gO"_3tSTRHV4tdL$+ߏᇴ(u\Q.`Ab_CUd6 7Ujq;.h,SՐ$00~leR$蒕Ȓtdb&i"E:w > gM7(0L"4 ~ϖ2?~Mi)0]בlύ]r,L"ɍx5EUЍÝM/pc:E[jS(p1 2MӱY-b$A4|gقWaR֐&vEBFBd4U0+ُ*gL0M,ɀHM+^V'U  |cX-*VU=‚p2M8fǕ5~abR~ 2&塬 |:Nb.+BvWaT{\h I*,@ht?lA8KX,ffyo?(|OI%Ił/b&ib&n?F*E0d Hk`*&bp Wx=PuٜP# K$L$*K(m+Xl .ATu WgEO<ȒH(&h2L݊L4IB88Nm({9㗏5F:A'@/#>}  Y 0bL'>usNtkKӁԱ2Vh~?E nzI!>DrjnՆd*X`(VzlI@ X-_\pv  p֙>k:LcpnZ?Y)]℆8#Ywʂ Yv E%|x.jUH 'inR21-n!mߧSnoT/.!9EWBWd,E~MXTDu >3lY^$+$L}ʇ`{{%p˄./!w%`AAᬓ(ir b(^_rZpkisE[7#UBb  dYglA=pU+OgJqx $K%*2M[){)qpE ovz73/棒x|rT2G5&'8i l+)^(Ykyh6OSy{bO¿AANpa R2}lۖAWk;w/)SBH_$W;ؚ'(ԐVt9e #zbg@h{ՓMw$߰@dM8 ݾ=৬0dq.*\e\ɇx羡] 9q_>Ar) ֈrAA6^KSݫ? ,_,DXL%%,&t&aݻ{!1znKw./C MOI1e;jŋFT((+pl)_KPXCulZ_IEP; 6zH{&|OSQ\FT=s|{K )&`+2`/q<9b?QXcsȀIL֙t E,x60&9kf2s)!z5$3~J$mzuቛ~\<S˗z:IUSާ% ǽˁ+4}XFޖELdZH@Nfa$qXr$cpJ_~v.ɢP2{Ce swbąSXjeӪ殛m#YQ"ޘɡG*n6mۑ2O}x pT>yZyRlod+G13^DKsqq?9&1mD|(eiÇqa bMo%ٙ'ƶ3'BnhۇvG 8B} exΏ앮1q~.nvΔ4R)[2O~=B| |K4T_s]f+vm"Ѳk%Ѹٱ)6[Ezsщc{,t&C:ֿ}:ϼ0즏" K南o[C3WyHz.;V g)e?MǜDdDW;.fg|/ҘP |)=GP1؛qZ< ƭCt,Y˘2|>Z :|Z9d3i H8p_Ϻt 8_gm8G[׃]U'_f΃9eXFB|󰗔ADv`1 ŧs  lE???./XΘI;8t u2>f WVr]T 4ndڑ--y3~4Աb3sh0k`;opoOɀ#aPq*d[o1l>{[7<D$ g\yƠ,+)qΥ 5v`F)ekxcS`Zw5yo/bS/m:qI{\^c,#yJ勩O"(ScpuTa &+K7qF8'ItD et|?^W̄9՚kNqsx}?ΚaN2/I\w\#I'mFV?{A1=G>!Gn_~#Nß7KcC^e?`}Qb_uȬ˼E{Y#صr#ّ8qUlyѽd.ΣWک%ќ/C% er_-x'ǂsf7oĪ )Piھu"jl3C|f"?21i*ˋ'꼫e0IɕN `;H 5i#Gel*Ț'6VKX anVn+xMm؜OAoR?#JLp?/ZB!#K3I2{M^Jrnx2.'['HQ0} ejWOˋ#8-O&l/q8IsHUSB0]mۛeh9a> r׭J@j7v'g+[J9\?s\KqklÛЭHQ#K~V}m>0z|S{0v߂˦D~Z i8% c({2Xi"\JMjh boKfT2 0P y${`tʚ uAiL)ʠS]S[W去vCt'`b"v}Gޘv /0 |G⺏63Wx⻍a<̇L>Piz3C/wm7b(yd nPm:$l$?\%u3 6p"XiŞ]q aV9Dhv4?J}m8uq0(βɋW}HRE,D WϿ  *g| VψVFABxȱ]JTJn%2Y9NZhKP2gpgQckVf͚ǐn Ϟ W*aЀՀerk#5Jx Mӵg#r&̨Kb oZ? ~Z!BQ~$+KKY\m6f!|%2ٸ\d{!|:)MO#Dž̟C3_?3g,(9%qcoP1{X)(u*ayyf,!D J(T0dx?SF Ed5ȳO^GW3VzNp0"*:% YZs&G2)a|r/]fAtSOP6 uW @i|ԣeVZD3cTM!FZ;;Ǣ={Я9bw/2ty߽{7!\QÈ nC|ͤ9YtlJD|vd-ݹ;Mt%x:d<ߟ$7,.|Z8:~{kß|g{5*%f\~Gk"NH[*l2W@ˬJ>+& tSj"F$ 6`I6ڼi_ żt$ I`M< IeӑeTYG JO%>5ay%בlJމ`ʇV3sY0+܄:y&ϞHLu~BO?͓/MdVI\hz:]C3Kڣ'Ʈ9[v 7_^5,n;c䁄}XR:ر|@V@Ei>d/dP Heo~q 铹'^@?.2kxD;̧4!z攰֍!+x<3$o<1?}Hq/KhȠsyܓww<1N35 n>u*$aMN$˘)E a`kқSz>+s f=d%8; >ǖ8`drYb+~U3lm\yfxeWV'Z zn97+0a/%|߼N3~5ؿ_;Bo+!]8j#z$mdj?MhB&*],(۶uD9[1K.8c׳R7}c\uSC9Sa\|dL;~fBiqNDuAd~҇'I.]Փd=0 5s2sFM|{7ђJ8;֨iNj#aw <|ltƒYzg*Ju"z< [8o 8UW5 k4u.-õoRRKq @>q˕t,p^q"9+ iHrq ~ATA/2'|m|J (I'XWE/ݕ)qoqsjx?_ ޴tno:G#gp$97׊I'xRZց$p4^|5s&lH7gVB<&ռw}o  N0"L1!zq EH af@=G[tDFݔO)) Z MjfInڒ'RI ڏY`wߔrAyl䢬C 2[4)u(,nb8={%k3 *Kg Ţ$/7u 8"78KcgD}_usؕkEuv(/6Em}jv4 T(i*4EVo3<$5[<-%i>Vhձ+XۘܕDlMZ׿2}|=w_EhsvU\i%)D[t^mӱ~85t` LL]]Iٸb}^EXy|s.^Ԛxyf#2-yY՘Vb}͹&UO%Y~tnͳ^MvI!v2G+&Θ[y<0vmx|WbQfcMk67TE|LBf"A_ju>N-J #ǍV{PmpŴ8=kG¢EbFJϧO0w`eHHTG+!y aSR?]TI7n]nSܙ_HBL {ܕ3fJ1aW]<~~}~.qsrG8dp~7<+I@ i"w|1]|5vLIhU_F_AkhЇ4k$[AqTJ|LLn&c%ڳUy-ى 3-]E&Sqë́t+ۄU }l>W۹f$ݮ!'[ pHD8@%8 1"JΌ)&O?X}4C=YC fox[QY`|R< Lx fٛ2Ih~9^ܗ0n_m"pO\Q]SI;_g՞8|h+|8ػ Wj*m۲^9Nph 2iW[a͔H۽?Xsպ%DuV^膮3ACRT,5EXбො,_JRZ-؎ϢR9II5-OVjQj**+S+U[%{mxI2i Kށ]n7qDa!t)+D+)cf4=(@qI)aVC0KݴoiѿG'&'շ-g{TkŐuǴN?Sv)s!E=BZ~Ϭ]/"Ln /GAt+6H5Dclh*,憇F `'c]ܚ_ZWBb$Y>Gw96կ'%!KDA8 % W˖ƒ-qaOL$uh,Nh%%`>(cѻ7ңAA 7"%9: \wHS޳C7h}~|e~p$.( FIKG X p&sg| :1Xw\[3{2֮ZDѬqە f< &t뷦mhsa:4-[$_} @,  iF4r+FP'uYN颯IXJs 8NX҉AˈXAAjILT$iˀQ4O͓ O 5l{غc7;veQXTLAa1&&aP?5uIBô?5;K: r%vBGZB0Q&P/wAXXkX|5^6nnCthӂ[RaKVXGbU$] @Nr%eH-*٤UIfIRA#`A.\ƴYdŚS>1g_B-׫+:=[CbWcXCbV&t/zA-Iѿ$Y"gr}TI8XU7ȃTngk빯]Mۓ-4HJ`[#.jxSN>k' ݻ:}{xX'*9JA0 IOґcUhZ7RF^NIPxmq4K *9!O!&<>)\`G}8/o~´4;zveDGFJ27_A8v hd2A΍p&VIs{0LӤԩg0"V͘'Y \wt 9iZ1. i\HFmZVoФ >]LVH ZQ4$L]C7kzlu&;S{7r 6v"kdFu%=+\f ǗeڏE l;}xO vbBS"gC9Z edORAq<.m~ϜÕ<ٿNycf0`_mm:SG(cì%kAcKwIn7w}W0]tFu{0iE% |iWq۝+1qĂmWN7SI{e.hiR7J w,1}_89cȌa|AE4ttdTPl A0$T +MG,Whu2BÂrb=yn ȡ ~JTz\P#ٝ8U0tIV6t4TvS[3nj^J}`wر(5u/~Ɇˡ <{70oW0M͐Q劫=`r\L42FԲYdzN@hyb 085hC_O(K&awرV8IJ%QRtT\M4OnIHpw ME?Syp&'X3oZ$q] F?9;0GE~M措 1i.wN(ǪAFjG =Z~vf?p]Y,_Ͽ.W ? xwė% $JpyD, 1)Hlt;t!QlmL+$2/a)!/lon@`w'<6^or7VZ^z #m )V{w4mµ7 Co4q}h1[Ųy&u"jx IrҼk#<+=k0Nf}'Z[:gכʈv gox{!4 {\9 S~L/7n#GWPJNul|]?tfI*L`vl8Ӻsor,㶺\i]w^EE?Y<˚) - .i2,\}h\@æmɡrVXg`Q4|R }qyՉ,)ݗN}od̞tz pe~%>f%MG@?|` Oޘ@vKtf&ҭvqnOoHRPY!XsJz6=ԁw6IN?ȌM{)i݄`=M$tJ {ZOhdF^=줇C"k^`;\$9- D Ť Ha]31_G')ǖq ϼ?3UqְP;.Af\괹k:e`}i,y}ofT $I» ̼t5c.w?燅mh= 0rytX59OZ̖DoSq)G?/_}-`}Kxx;k#if5 zG*'Koψ[8ڎGg҆vbF{<8wv%Q(dXWgjHvo$rݽ!zO| qyjM^K5#,ł$I-WsW%Jw/8/cn了5! [p\Dr ,h1$ߦc.G\S~‡5g}o-x_Ougë9MG:I.ы`c8v[\MnNNI|S`W#@ti3?$ H ~A3{v3Z1'@A]1isv-yOȾ{4_m*~,I 3qK>]I/z}Ӕ۳BN|[sk|/ˉ=P(oAkL 5{2IN0>t=㧤<ܟI镬;Lʚ J$x1aHG\#3cUlrЁts A$*( u 6U0y\&BS?;-3n sw!F1*l:o:(8\]2\M]4%hTsE  -[ PG18P,Oi~Ų;I2 C|떗Xɶ͇ɨ$#rN7Etwp P60Z^#==ccl9Rrf2{J:FWlq3"n}g&qC'Aخt.=P2)co͡ӡe<]ӠHFGG-M1M;j;^8NįP[Z,KowOYӖP(ڏ;JP(mCYfKnJ*<@d} 2'&BM#yuFvMc.{9&]Ğ,"Z\$6BN!D WWQc>wmb㈩2H[`,8ɥώEǐxc6٠U`*}m67( Zʫm|%>JhK$*۠N|NK md$jr8t<][__Y~;| TGL%k@W`N_E{V(8ro+ |^sX+n/$F6iU rbZ,10Tg;{TCiٮ(f !il/l .s&F@%'kg=\o5>Wx46MktbUU~,b# Ph+pcPSYM~):š,F8_$0vV" }oFy<1 .$ǖ?[׵ bk2\ {~;.̷/`ȟf6[\ПKo%8n' I;dۓe)pӽw*%(t*m<<rݿ~ oR)炼zX BhrhO tLKN΄^v5TWxٳz ^YEXsqؔלڂx{OW*gú|[]RXi(9xk)hTّN'Z:?Vʏ7p DU.c lG^es ؔ@^iKuփj 1ԕk-IHvHܢxB!1X]+A%\gvKq}_ZCD\M{u{[cõ3{Pv+Ufg!a)%VbtD>i9;2αCU$I1H 9Jb@ה<^`,aE|_ H/ŏiSKgU ŹEc Bh n uU 2|vܵ2'iVKgN7AF<>rsҒ92{=fiƒ,'eiqsUy3 cP Z*/h2᳹uGyE3a05陛xGyzrS*j%ilF:^=?99:w?66<#}L.*# šNc|^|;b۸iL:#ðM-ZƍKg[%NqZƄ,O<$ypqTk ZbcH7t2^_3V7 ntOnK+_{e-@7sIaMj*JIKk[.uBh.BlIpiΈ?ۖ@QLi&z!1փSH۩.~/R:kTq^B:BX;M\: 鸜NHq@ѱ72'F7%1:R[:DHqpC|\L@h8u7R QpܶM!@k$n8n^ VtFlnޅXB6n{s!4b4P3'ߺ>9nԜƸEgtpxJkSyFb` !8 mk/{5S0rFAڗc`҅N.\dj 2y_9w) η13<;ko"K}ٷEiݹƬܗo*{@#7@ɩ[ח}EmE%+?8`TłB`f/d]r&k݈> M*lVo4P0y{I^wixydMȥ1nTA⽵&{5 3,>fNzr#-Wh\2H')ٻ6ƺDCu Mk(C F% * -5 י3VgpxAU39YP?fuvpY& &8Sm:j@tQdaј<`lGYnqnDʖm j\2Fdz[(;e:zF`bƐiKO5([.پ5_ȓR%w3rN&ٶ5;%-I7|:Gd&\V/70l9CtKL[jQv0@TX,ZkD):,5-r 9m.d_fYX^S(hda-v)*D$D hI铪)V}Q((|Vz=7: nOs=1 Q&=JiyNyqt;% Un(k/3t-2yt .-X+iCx YkNzf9hB$5YV`J%!Ak;=咐[78[mN _GKg,3xF'G'Iɾ >tdv1:3.t pJ-5(d|$ƴ+X,rz;y"]Iuu|$ ~w*AIe犪`0MKAӹ4['=6N: }}D['AϮ:o!|Xw9M`KA>fkӴY&?{-0&t9Y˖a00ofN A@r_ dL6$5O- QTt'GPq_otvx!aK$[&kMtsk-$(_%V(_[Dݔ%X;oWW SEi)a$l$w 3꒠WW/0m!I\+4wѨ֘1Q;[hRBSѽ&ƺxVVyi@KЉΆj$A܊okh)Z=A54-6ٽ4 y6*iE#׵S}d_b <4JXzST%ŕea6 ƭ#u6a@Kr0\]CGـgǹul=hyJ+:_P|mѶ G @)@DԿC*ͳKzRhBj/5 "AYkm;$降ŵ_WuXRi٬i-TCWFdVtwfik3"%2?7۸;CbGZ7.:Yq"pJJʫ$,ΧdM,#u'\oe҄.I"nMFYfB)(eQ`,XY$.(k׌6z/v\=Rz? ։ oPͤbA6`P .KΥ44#uGL֖Uh@p zHmėlmg:2=2&Oҁ mv$Fi$i1@P .@8Fd4h?tod-dk5g{)ݔVKDֱͼ<P(P(ަu^K&mб A R"ZKFR)գiqhk݀kl_ IZ]6y$ 7>X+u{$7M|֦^#Nõ{B#+bY.^ pl#IỎd}rHKRvd&-\~†}[LqdKP_^79(iֻ!/uН0Qs0=MQ=ģ]#hEe.9iBG]yu%RqUm4`7¼_t⬖1Y8]!g nbڝD^S(Όy~okX"3qBP!eǚBP{ şU;˽0@s-#8 pX6BHN'P뒳.}qBl 0Ȏl. fSi8>wŶ"ty TMAj([B_"B%0,KBOS@{eݶPmJ"C'RwY͉cX8p'fDcNp P^% 6"r6fc. ]"c6=-7W-%%5`ks!sy>-bjJ58f)HpAU$v[̈6Z6DG Dk]VyjY@W2%%3dvm2׉Rs.vWoLIrJPGXP|m#QòXbuDHشAI* j 0jXV ,NsV`I-bDa%%VXRhemCe_ڤW6}yvӵI *$MHI6NmJJZ`%ODFrH%ehOZvzl_-7(jZ}Alfm ̐jHjض&U1+vY_Ht,% kͨa,EZ" `K@7x:rɱ5RsεnJ*Ό;[Ƹ,ҭ&[ Dx:V45ĠSz6(% ךC3r`Vx^o!"V>掿[L$EY%vȦ 4 ;M2D `͡Ҷ_?Q:jTW[lJv{#B~W?Kp@ֳ\xQiYI:]|oۥ؅tlǚw҄vXG `B&ټm'5n%md𣧏d|WK:w'SP(#jB3apO;}x&~rK&Q=G2gΎ#g(Zcrʭ3 ?S~oȧowi(ۋ)1C&H&`褠" o US\RNU4m $Hˬ_^BQUـxVⲚfBJ$`,e%eT4gH:pu)ͷBF02~ޔsŭ.?3flOhU(ڃ=%3]Vŷ3~bgf.;eYDKFM˯gwB jU]9\Y3ęWsyz(/ I85*W]0 /ԟcwZϥҥ3:~6]p ]}w-*YeY}'^w73^|a;ĺx^sCf\1ﲻc*|w4 rffžQ uQ3øKf3?!ĞyaK>{C'}ں i?~ɗ3ȚBF/aXsְ̹[?1feIuU"X8uʒ$F(TX? E}l|&:}"/,k gGp.VG RZ tk2̫yoߡoߛ's0ҏ T~E>݆$T+⏟W4n{X+%%'w?|W%U,?[_K/_blyVk/_ }[<7͛Ӹ^>h>>_J!>; 6}I$#DŶy_\χK؅⫉ C4nsj 2~4x =2ڞ2=IQRY3dl=!#h[pBW'Q߲*Pml r~jt< U;Y>̘+g0+@xɞ5F\=h7p5d|T"pȺni=5b΂aSa-8!#]3cm5JACyLq棍׶Gy7YL wt91C--)DŽecDH̺1\qy6fUZ^}fqx |Y7X{"Y >|q4,`)o=2Ҹ{sŰ`ɧ+f̹6Y~S'}nOvX Ekp;"k~gfh75%~t:%oo:,F~\͑=Ѓw ɩ#g>$nZQ9Ѳd//J{tYƄ$544 @ce|~ q*: ֞7w< ғ/Oi WR"1DݱI Ŕ |I=A_IV(83o|YH9t0ӧ.UkHI,!T$dI&Ws@i6{9Dn%t`bi&Dwp rQ+G > Pk^ TR-玉)-fo-7}!Ju$z韐I0Ǘ,l+$)Ƀ?/wbۀB%wNkį'I:-U?_̶͢9T:m;Xٳzwlb0y`Ka%lsU(Gyu٣ E] b`P`;B  1d-Ҳr8q>gѫ{:=2ѻG9_&L-/-E}EP!ܤ`#IO$IF2d&+ѫ8I`L?5MvJm!G'uɡV^FqWpBv IDATpO-|#.cCDx'2޻Y'q8NR\2>x`&qصX9%Ni$*Iݘ5 fN뜶[gPo]P( bE6c#T8!^ ~@ˊbFS%+ɾh$(m:P(NyyfC֙m(P|h70ƽh%BEV[CB)WaPTTIPMcL/4 >bᯨ<*l 惷>bU6#=^ߔ*e^^^WwcU{E?r©t4jر L$筷֣#ʶgvl^rTp< Caew:.@JVq }IB|6coJo ]˛K ^ e,rϏiq<=":2B9G.3zO%y^aU+KlGw_ݜ7t_|Wu;keTJWGȬZ'wYxbUUY821:7vbBGTb09XDE$/"پ?bvT)"і> ;9<| ]t7ufK.$Bq 접l3[-$`P|h,dҤIp 9GMXv-[lᩧ"77Dp%[>ZƊS :땟3ƋgHKVOy()gfvh3o*EٱNUUe26hɃt\z`=SץЍ˛Er{/غ7 x̳sy{a"YdѝNV>eȋf kZWbʽxl#2>l8v1pvOml=յixa8Y2 5 7غ6K7O9&E1fKU'ƥ+DhLs385=;y*YwPRً/O'(zE'1Λ&b;nlsX!H΂ ِo,{#r-6TwY*wɠ9=IegkRÖ$p٘okz:;`a0>} rE>^܃x Y@%h G{)?^HIcEa@B[' 4 }5 C0Mi\pQ3Ie%.fcC¥7H umZT">)..#ӏtIDKq=A!vm> :TP^ YbNC9 rz*ڳBp^馛ڣ1b#F`޼yTO%-f0wHDv ml Hi6[?,|\GG3/f/?8{G~ghݫ\;\ij.jc&bu L9ƎLzxx#E3!ݡA?^9 ;L65``xf}Z .y={ޟ>+\~O~"u&_M Q{MHy_=*bF^_;̽:Yw?͜쳢} UW Y|^ 3x썃j6=\ '~҅ۋOQU ѻ ~DǛXM7OLkb dӋ:)k8u5:0*줣AFq!@ 5"(h)}RujtU_̷~ XZB"iù4;H+F?k y,Ho}t$ct7eBX{<ґЁ =&Yl[* w62]H/vȪظ4c/cݦnKa=W3&aNd"ygS1n#64 ^¦bkP@m?j]GGHfu1:_ݼ`?w`炥lNufƤg]uQnFfx19ƿ(=HEI3ʓ(Z/9F90te;*{[nzhBH @B4 VDP ( E: ^B 4 &M$E>>ٙ3e9s6NE/ec61yw˩8 js v]ceq+gs'$f-9f݉x|d!/6>I&c(V S`)qZe]b)!LTh٣ L$:9 S۷H$iˏ|ۺ3J3N֚Dg8)>~#t M ɐփ4kZKEEEEEE_\>-J@n/Bc.&NΝ'Sf$h]#ieqIGQ))Z?Ƒ<('sSz Sزl6Q^\1FJg؃}m! jfA75g3Dn|i>zT5[uKq65k/>J$I D!"?7϶t8hҗ]ql>Z­| ~GÏuCrW}Mh ^h-۷H(~cZrLZ~+@%4C\֕Op8<ò6jM:G\\8j= X$#~oeKӸ% n+G^Φ܀z4p!GiÈ0p45}9N:XHI Beruq}z:f)*0qHʪ&p|%4/qn廘@ Z?w;6XF\wg/Zjmwٝ/~\=ZىO {<4x07]ȲihCcn}p;xaià !.ނNvb7;Ќy0hp"7Ϥ:[s(Nlr= FwoF\wSw~XìxqVWtv`Wlw"+f}♆^ўD@ܒOޏ0n/`+<} ¦RǜK)5GG߿=wdw[pJtz;sEN F$PM&2F*,ǐJ:#>Z@ۜ[Í&L-^tcS4U`txqQu:>qUTTTTTT.Ӊ@NT씗.b:Aӑsԫ|,p]ˋw ͥ! ӳSG*سcؗSA~Ľ,-֭`9!$db4oDߺ̆ Ղ-7* kҊϢMLzv8Zoɰؿ52Jp;7r0bKk7 ,kvpotr;H 4ZJp>hu=E};5*O*ؿM>d"'i2j Ҏ@ Li$s0 ERNd 9x J :6R>LɊ]-JR45ɲ3SS tu: oc0I7zxUS0/o=kKhyT~@:P4 Je}o8nR :rxx]Ω$NԐM6^Tu%*k'RSK=rIDnDe[ŀRa"}3xuc:E[x}V7Է@Xm4܈nI%={,qd9eulOIf{&Aih5b**bY4-N6r HY;qkkER,T5\ {2@YXە߇To (֓X1je:4'~䙉)ǂ@Bhb;iI2ky3<)O!OsWETNadF%ZuOd%/625hlDa1 8pLB)0!;KEEEEEEEEE_۲d6 Kҽ$%h>gأYc>ޞFɩ̚F%^IRL`3 ԥ9{I9zl+hZУaѥ\zuyO׫J(9>U DMO'UG-{RY| F;<P!5Wxg׶xgcM g֊+Nr'ḻivs>ɍd,cqo ˿sNOe&v ss}wjVlW܄_pzYw}1 m:ѥёM4=lS7G!ys********ŕZ r:\nwӉ:kV $HGEX;UwFx63o` {{ǟ~Cn/OQ%YF6ĴG K\}=^p36o D#@j* d}^0)jR||&AWLk$y/w,NUb{߽k9"HmY[ E&0N扆Nh, N#*kO"/{4Mѱ2XɓOdK1vY*v,p NøiE;T{y ;p~NDEEEEEEEEE+[ s1!uHjHZ-:E^F W]-]*:t+-YFToyw[!]}&/RCvkA E긺`zuj^ѠQ~A>\>D\GH*z.ȝH)6I! uTCIѠaw=8I_6l=&S4:4Kjue)hd.P2-1( ^̀x(~Q ^|Z\V}n~z [x"[s,yy^",U k׺i,/\i9H[yMhA=~!Gyqp#tR[~U`=h1{-աs1us-Z_\`VP]g½4h3e- 5Ho2 ^/#@4@EEEEEEEEIot:;v,/!8ޡE֟ώCl IHp8ir݆r3Mʕ+/j* d,  )(Ftp $Y"罄 Ʉ*aǠ\V8W5ZP+6LeeY%<.n6QRnƩt>]YNKupԋpp %ou9$e:IIFQdWC ]Ғǫ[YZItK88Dx ƫM]Jm*OlT^Xj并'zn) U.q$F=u8K^*vJϜݐU7[P+"33#(-"88CEE?U_9@nk?QșRp4V "#jBV\lE"aR.ix/{$\AaZ<}mŮc@I,H2E+ZGuH2SIFS;?IA[%)x8I_KZqʹr2\ܔ;cѯjECKqU(45^xGOOhYW:%"R0DZ r0p:@pG:=μlP4H‘#]TT)xv3]ܚq Aii)]#KwdC׾37Dv삮iN](~uMEF *T4Fd Yv-BB É8WEEEUY,fd<(EC׀F>{) .s7bhr5RQQQ#KzoO|<20h4 oIPdFe4偷zYSTTT\h![ہ<|N{q?a~Oôgr&< B5UQQoj26k5 IzoFEEEUwh$I,puZ Esf"Vhp:OEEE,KxutZUUQQFŊ '- ]י3:³qc ]WnwK[\yXyO:m3ECCs_$&dIb=w{aJu8'N;]o nuydUQQ%+ڦAE****j4%SQQQȲROE`bͽgO7߄qQFN?#BPUT=ʯ? F^`2[Л7ߕ+\:0cƺA|***KJ8fGTna0bW|Re(@9C#Pۙ\3Mvo /Ʀ& KIK=M"0s祒RIhFVveyf魧Wj>MZR[@\f1}}ڴFҜ,vblԂ6鯉$$5gecC"Qw'[٥]LW&VK KEfɀ悳@Vd)3 F8LF=sr;)+(Q?0_ zI:msF<4#Ls+w?Ψ.U~j d\ %t=! I)aH=_Fvj.ٵ nGK8DAQ_bИlv&ӧ9ymY1_sکFr @R#('f-`0o{PnE?@=w!@V+D2pZAMs~Uo***dfw=[ݮ<ڷWݨ 02c) ƈߟ@vk rYR3u{ 6'p:7~eNxgu"BƉ7n#X|Ǩ0XR|h:'?ex /Ԗ囄2R@$>_uZtP/ףщ=KWHZ4D>y~'F>/6sX'N< t(G`طUiu=Cn*+u˾gqB߄#Vp4 ;ӯč6|14 Cim8BbAXsNpL1Y6ˠG{E$إ~q8= IAI@ًH#(KܥȰI8:6yg%ߩ| nǂ]dpꃸ{=Ň)z\qu,umiɃ1"Jc>1cQ]DTcC 9y{e1_na!脉صX,No(])HnTʣjЩgnjj5X[+ʕ{XҥW=_]lv.%lhkڄ(ڴ]Yzl$6Foc/)5Gk sHD-DNH>9OQxl/_|gOؕK(q %B1w}hwpZ4ձݨ9ND5.4=#/Y2ɦtfrЗ-sI@D{B`o^w7"2]m阋_ϒe$S;8\1')2=C!hKёoYxď&?DO'Y~͌oCi `NeÑ<0y"sP3tJN*'%qM(sٲǏ{L;qN'7x1-ĀN0{ ?UUQQ_B/Suy{+jjuys$K>$I";;kגv%tKbb"k׮%??[{+ @xV6>b۵1 $m"[`..Ì$G)Y9$3S mZPIv22фDnPz%">ϓ-j64J F){m2w<21!/:7&3v^MLG;q2/o%Űq:9ԲΑ?NQ7rutҮjŹ"K$vZGsDDtMW0OPXZYqkϦ3tqR9AR))+3c;sO:z>Ƈf; [-EAJ4zK8C,I挜J.jLmd[Clfc20y_X߃dR녟=_ x_FdUZrZt\bII=/H,g?-BCh$'t5O oF'IO D3.@5 "`(#e&y|, oaE wav'2ivs5ҵ^\f(k;?%|730 Uyz6j 씗0zs>Y=>ߜDQ2h e8On[?Ν(Dч8a PN$Ro", ҉NA{j*ඦW)Y~= {Ð4],b#>$Ly 3Mp7D]m~O\:sc>|}{#i! !4t8WKV5Bh(rbw$+FI%y몼ʜa5H*KgHLSܠύVcڂ},qGȴ6o5ؖ'WEr k9jx{Z)qC T)v*LV#vuV^^xز(@9PNs%/sW牏Jqұ(,/O6=C2[#uSOpTkY&\֑GǙӍ?$.<=3C/ѹ>$l"# :tx{j ÚSG-rDݺ= detӺV,+xxk:NkyVJۆ†Sus%WQ*~˼Dy]Uir8C+Vrט?eᇄܭS&>Ôy83ڻ򤝳I5hK;q_q3>emFMc؊],=ѓ-UL *8s0/Z·7sjy[-`/ƬVe 9yz‹4 (= ~g -r,9^᥯u#P&mY,ceUb$wxv^C^"3Esf2nbVdxm9BռY<ܫZG~m/yy|KLmZ5XV|xM,TQ:~˫BZb:JK_kf0qAAf2G&ʌe;I*q8rE]!Ec&hBq `=sgؤWy .jT7LO[OBI&k?\Ifω$n^`ʺ,_4}m5aR^+6v_ިQ{] M3!) W No|Y$u1N_[DB`w1=gf^Ygj8H9_D1Qg{(4QZZt^׼?֔?|c/_r.Zj@aF!횹V' Mj!?Q!r6Nc3zl:2^m1 aE*5e%`5VCڦ `Mio2f|нhv Uz"o[ >ɫ 2b\[@H;iVhftjPNԭ[W,,Ϩkh1J.\5Ʒ=?ǎxyJ(h:4:'[.T;#S_gs]Mi|z&ɑ*x{zl IDAT6Zi/VLmNs뻱FѹCIw c謭ߜ&ۘx'VQtY/t>l`3oP0m +ѵ}BF w%W#0z5&s Yiǩp5W(q {a!#hyO~AL,4MsAjR^KY—hMPF{U eG>2a1j  3z-[3lKf9qM_.tl` vD:<6,{xa -R-F"zsbm6=]P_ֲq?Sҋ x"/mD3grEƴ{a>3Rp۱*/!\^n޿ Ң"*n|:,)Mpذ9EX>=eWB2bʬNʯmO996-Մaw[ÎWR\\fA1QTpĴF.{e)e8SjY{n۝ijt>*2CX,Vau tR7af]\xi Y3"JEaYX/ r؅j p:lbZ)n1¢RQj^Y&bwZS^p.vQQV,ΖY:8LTV39[zCXi#]-6aDWhT]Uup|a^C.﹌lX-pS60[ݷV)JDq|9e%}җbkoRsV7]|[e8[R!,!;-\%SXJEyuǰw!Jӗyw`NwQ0|3AU!DuuV/*DkEv$*b%f1U)k&!ڎX"mUbCDaog]e*>St~X_,(;(^vIBk] ~[Eϙ{DIfZG };FT!)z "P4\\aaB]}7.Y8T&]Xj/VOKtNd؅<[Ő<ĝ}krkT葛ĵU\{qͳl⵻k^+\SjXr)=]|hqV!l'ܑ7c\?&}UD1ѿ$~uhv17"i:Eϊ/+qJ,ytiXsڕpES!)F̼F1taK}ncC͟ 0<&n:M Cq*qC6aNT1}BkۿxxjP_1z7dN)!?}\D\"j7턘;F1|ip!*}M_oӏE{mlͻo#Wژ?7S<;"<8B,Otx'V-G.1~Pq4aB8n!2F=8?=eddZ RU_Ntߍ !ˮ˲B|"!B-Dy+7v=?vu!ڴ#=${ }^h1[DknQoC^z[gĬ.7sE/D[{*)Cn=$ƽz| ?zńEs/jOvdOwN^tQ8 .[3nQV׎O97~׉nQ"Z"^y f],^;Dt]LE)(®,z$nMdZ(xMm}hyëbciU+NEd4:]0Y^X:-zMPE/^z : HFj3獷n\E=_#z% A)wÑ<|祫y.ԭ/w@Bc"Ji[eڋi8QjueWYg/^bH/=ܔM^Me??|=MVv!H١'=Ce>Z7]|z>d/ tzWۖyyY14x{^mOAΦ3#:Im,e^8Mlb)۶{Da!OIƹ mwpF7`.)uc~YƉ3FDTm_¡L}FbP= pr6#zA{B57u6}cǾMt<إ?:|\<朹/c!׍Ъ}l7K˱$'1ᴭ~O,8|FXD3@IJ"RcU ֱ:y"{Kz'i÷Lj!r$GyE5r,}VPDahCffSߔ^Prp- qsT)['ǼaQ{jqzmB&e^wsO+m&;bXѫ5y[`>FbY0>sz EQa.duJZ..:AL #8BQpU7IM]b/yR `Fa[I'wT7hEc *I'o$SZѕ!z!ǽOl]u1S. ;vj mǎM F+ж#ic,q-qt#^Ʋ$PĆwa=|a[bv$ĭXKٽ':*5(Ō.YU/ÙꯙWıUG=8QZ!wԻv wm[i !D+^EUxЏA `I~[޹cg;^X~+:DL l߲ t ؗڸ_d?[VQRΞe1vZR~Kb*?;(#*4EQ׊DQARA(U H %ɦBzO6duG !yxHv{df{ϼ;,,Jtw$6q뫬n6ǠX |b"d6P\bߧes>Gմ AfiIE2FפJRE|ɪ߱쐞"'{?7QEOzDZt!a4XqچU%0K|Xf` jNۅf ߪN%!8j!s6 Y֯OTZG΄7KS~*H:PZNM4;3=iBb @ij 2v.\ȼmY~߿힀cf2V ũ$V1rxT:5bdy@NB+ǶZ1m1aMZ~s@KԔ\bQ*X3g1KTcTY֚R^Gv1GmF |RN&aɗo<"O+PJM jf _'ۉDɈQt"ddat ;)E\?SB 2G`|rR,xb~:pu)#"kө FO'b=ŲKXqDe=D^-6 JϊDB B7) ͫkuqe.fqkvL,rwթl;jר;(#]G "1: f?8h9,!/*9`Y&Cѕš՝QSJ&;[YBt;{uƯBXNesA$n''inՕSGR[_.8@Ё{5w6'?zntG ׍©_!Kd,!r3HHH\9jQf%NvFHH`%e86M'2r׾%XL2)B(χ5(i<̧+d3*h 6'XB-rqēN1bW0%tmH}ohЇBq`FKC[W 6 gfLV&ok܃Mτ6q4DzVk҅@ ɥxGd`pk 4 {ַo0I,T5-Hj8cJiZcNHvM-6ltiE:==Ffdt-YPwlf 6CLh$?7ɨ=%E gHyRWsЍ׋HIjE>qt?| 5&T`ݴ;D Q=p]q(7u#~{TՈzBύTD>6W'2ڏRp?tg2sAzttwo؇jڍ 0" ewx'r 3cG#݀*RErvNŊ`ű,?=NWHMm{Q+_԰ 8Nlg?{Ǎ_}/|3l9{+q'SCY-t0(u$ls6su:jmr pto,mV[O.^MlЀJk+>~Սj>۹x i*+jxy6)_&Eef.p/#y%$(4Nx8KW Jwt~z95$ce!”4*u9rK] {;ro:ƸqfvXkYlvy~-kK (\p!lTk#qԓNўȰ#Cl9] ڨ)JbkBQqj""6jd"q^jmu|;u;?.`3a0QDB5'W|WG8pD5yQs"4̝UhF2OT&~3"WڇR[ ɤWWd%`)Dټ3 d!2>n8U%`.9ȧn&_N\7 dx=o?*&]&ea;†c%E@Pc0Ƒ+,dA #^u"X 0*pb"T*;r kE!dV5kqi]Ή"Pb AAȵ;٘f@mJl=JH{;5$dj5SF&up"s.NV ^v6):L9RhE˯ڛ'^(4"j%h 7SBY0 r|ڇ▻ǫIivnbt!KEaz&墍C x}E ψSGSL7"6~1!6뉲K\E||BQVΊݲ>ۺ.43{c]%fa|j!M^㌋iT[GƠcsqƾV9CTi3A%quʼnv ٶrqn}ԨXV^カF#2_TU`sqM*VG~ {dsn~x[Kȭ"-ک%ap8練x@&jxߛ/>ƪ?E'WaxؐƐt-g9H h7i^x>C4}Ve5Ϻ)Rw %P:7@//=Mܹ w;1C^d^NBc,w GS+Y{PtOp_I K 9RCPِh}F2at 3SvC>>t'1}wXv 4pd5_Ni$aBƠ'`@28*2H'wa#m,/ /1-<3r81#fs,[uɔ˱t+!u9~]6!M p5wZXu*AJfp=4mг ce̽18bI'ʽtAN{D{pb ˃S)KW=[fp6mUs. ̞}cQ W0/iHBmeiFIqh906fR eα7]<̲MYјƭZjZ !cRVBE^ƪ}? kse62Hm>ϛR3gO ȻL7}.;RtGX4!z^J{{42+m R2QUOvv6.q5 rܬ"6|ņ\Fa3cLTTP8flMXZ6#e5(]1hF &5R^Q7 XE>'aR 7϶?pwZUPj`bhj^e]69jXIIWwsJ fD{=6f+4E+5: 7!f}9%fr7W{63f^ FRnz(ܢqR7TU|倭`;s~.Q;7 l|s<džh!9Id'P}25*}"e 7J-pd{|n͖kX1nvĽxN da};Rb~xnՇ3ycC1 za6KF[pْ<;>/DCfd~˗OX8SOIL &~c[hi|܋|Gn}n$E ,!q~*MaFy_ĥO0x0FD-v :jT.hb"ל-*0hqV!'웮1XiNjJ*\=pW0ZϝWԢvuǰM-号6j'v`1vg1X@i:!dVWNY Vfl; SUef{<'š Z UT#:z4:XըuQ_?) ? g#d IDATx,_ 6 #a?wܑ#/SU8{zԮpN+ gjMc8ʎ2> +5l]LNXE:j8m) >Mj4k2>(w&.뚨j4.^Lr˧~͎ĿIKHHHHHSpnʧIH\#aɒ+]t5DV/0#݆J)j2!o߂=,q=!@KHH%H!R?!ɓ}nKވqUxDHY%$$$$$$$$<ԟ9n*{}|įu$%$$$$$$$$ ^zՍ{Ɛ8$,!!!!!!!!fРk!{{Р߷ĥ# `  28fm K1y{i+_xb1jtlJ"Ugk&}<#'ۛs6I\kj/eS@JoA_p#%+!wC55d$&M@­Zv3iNg7ᝧ&\L~ cngTV科Q~S6]y\y*R=eDxIB- g%~=[7組cz矐S"=,tkZE(NΏQnanpd~ݑl7G!9( 鴿Z*U!EMf=Nn~醑RK}S]%ym k_ܶu^ߑ#ᦛb;Iee%*_m_;)N=? l'Z1.h|geMʂ0<@el&Ӌg҃Xco"@o˘2gk`EK -}×_/i$s ۣshea8kMš~Ψ]4/Uqx3gv~;[R1yV p^?#<@|To`)?]bDiayL[2bhE_eƅ K[8*ƹ0SڝXv>-bt.D8~l4➲>_κ$3ᝣ1'tRF-O!cu ]%vw]~8T=/»Og?u{ccVK } 3WޝH_ω+cⅬ,g; L\+jS71}>*t)=3@ 1dYĦRpr8!RyO?˂(#:&Gz7_Ob7|k=Lxg7i VrWy;A^86Uea|k1w_3H)3V`7 z",فy,gNr ½eoLJ u 8^N|2 chIN*t,ν XJ!@[y8&05 \?j=S`Ã,0*mږɬRuvG$]ā J%iۃ=3?EO,dֿy32zI'+ b0'q6eɔ%.kꅧ8Pܓ^bAJuF 2EO,&p'} (q_fTTƣ<$TgpFO/{."g\O*>F=GBy_YOy{\v-.^wFGG*AeZ7d:'#g7[[Bnigè`TLOdaN'sJ}0i=e5U'[7 S \ДQhV~Z0JHHH͐<-jpQ^*rȬ 4*lk%]o hጻ̹$q ~Z͏?my? G+\۝zAKӒ؁.J̹Z7-f#TD<_|׋Ϯ!#5~ftKX<&G*=4EcDt8K. 2H:g$*W\U0%2H[ɸkגT5k󰏌#ܮrryb-ݨl79XƉnߡѣhђlH۸Ykze?~31-kRH6K0/KM%=xV,-=+L&ĝoT"/gY: GvyV'1oB>PO(OO-g}tjoE${sKz_KFAĺAij 2v.\ȼmY~߿wB 8r/urF#TSQx֗!D*HIǹ 56p8IO@L{5>Hi?1ת1RGjj)15%%z|hJ8\E7ѡ!HA`;JtjKq"@.Jy@n h8:NG^]'% KZ؋-{>{9HHHH[B%$$$S4}U2s&%[HVk>afRwcN Ѵ8p>g)mcīAXi\:?FjI5plC?h q{0=nNd{ k%aPYZ*t B' 6keoSdG0>l(k*I3rKܔPE'o`tJhZ7s5:sb_8 P;fs,4d2`GM&RSg?3Ff36 uĞFO(?%>PIm^Ekjgyة3XK:'}n;qL؅p(*XǚL'օJΆ1tjH]NB 9Mܿ'M?mĸGfףPJ>mJ~M [@|5UHqm|*;L8j&o0XWyVؚE{+_}90WE2{m5C>AEa1d2Ŀ,!!!q;it HJ*;.,.aT6m?AK9y"ъt mL\669Gvs^8Vd Ŋ)9(8 "4=[1p4x^^8W琔kD0ez" k m8\8Hl TD9 l@mi*ÆlmX\ԁP1 ;El%ݓHA PC68B՜]N=;|daL֖xo-N1Ĺ/p"6at*|6h| Rm)Nx[c=#" (9ڃeXD3z Df"S9(9 f#2ڇ➷ XE.]ZI%GcF;aP J{5VN f=XyI\CB1':c#Ôp&4S ]\Ņ:T8l6&[+ GJ+"M@čhO %l"5C[rhkNQ*s2y>=]2T+}qEK#R\vϦl"_Adžm75NRC?ggLe+䄶zO_)r4DEBGS:C[1Q*SB&uhS+#AH(΋`}G[_MT0o~䛝&?:hsH!֟.QuqL?;ؾCj%CFA|BPhYw (5z$13o8Lݰj Om䧞+ξrp><}F2vMy[?ۆwP<۟ᅛ[vͮ =f7|4t{UǓw !ևxD ~<\o-RM<|Cjfۉ';=ϼI7B]=tP2Bowt~piF߷3!lȽZ`B&Jw; l\fO꣈V3fThoۢ *M0YLUWQx@VITbjefo7؊VuzlnEf+(("6L}6,PkhRRFlATiilFPhP7c`QMZn8dlƪPQT򂱒"li۔8!O_H@[lV<]Ȭf2 fh4'?{ <"ޛg[7pruƮd>D~,TPζTS\#u롰͂*TQl~NZ W 89\Wv9o4*y(5ppD-ꩨaꊳY$%$$ 1cʹgn2{*pؕz/?I7ykt9VAnN]@=W"z/ń<7mԮ1|%BXWKxeZ\2k[]~b,eRl^ne`f9nԹ$C=,bP6r_CBBo#{S59.?񮑃}|3ܠS)CYؕ"Z5ow|?.@gTS%S*D/QǕ#_(bжMEgsږ mM6í!$,!!q=p8D`a5IxuuU)pӖ;P <8njR~fWzv#sC.?/27Ȑty;>jf'CK0i IDAT\~8w%:>Ȅ [Îl{Ո5YlYfS ϝL;&gތ(:Wał| !PyopC&]*%oGɵqUpd2J—Cp(ºH9^N|#il?o5Q/Y MGt+q5w@?c͏pP{/ÙDHK8$<{·vjw1vB5aږ `mJ Ñ˯j_p{:NvlONxgaƒ/23Z=_g{У__F8Q{2TN4.*85$>7m +:s6Wy!@7bn^/Roכ5o1tXLMk>cQ;8QUn94oo=ikrK8aj8,%g"?E_KњxyKea 2ճė?yX Prv15Maw[Kc<^}^57bs>PH֎E|0oPĢ)әw՟3yfN2MMsx\^|}2o.K #CK".RȔ/W~ߙ9}oQ'Nd٢6|0Eo~#ժ#%P,W¬_I5:Xv&wfNʲJ y?q (" mk- lm[kw6MokK-Ϸ_6b),,c\D2{AѭV_$.'-- b \bSXBM1o>L Pe2e `+k4/o # 8"'X1YHbRZ0kȏ{yt<ɵދR)sXkìdp{:2yoy?=tI,EOң>2l9 bTԞXoY8ҿΣ|fNxpIIHH\>bN^;=m$C:*:O5@8t&RŒg^] #F#[cwo1 YX.p~ak $6%Y+ qbڱ!ȭ9|x/%ѣo>8(`lכfJrٲ.,$mXβ Ğ`;+p*0x& % w  «+SXͦY | FCvswCWsBⰭ΂ <Ϳomol&-Oqw'zEQDٯBx8j($??/PBBb,|8FQg&Rp-Oo e܉o*$6E_u Ƈ@``pRs>c6S,^Ofˇ}ޥlOLv*ƿK$O׋os{/ieՇMй'?A (=vL쾪Btk(_.$$$.[_=aŜk:r*McQa!+1kf 3'}9p]e{K2‹tt~݈kHH5pN@E3gՓ"dxw~(cAd >9!JЇ77fihd/&tu`B#6{/;S5l:ypCmn΃X@YR9@Ϻ ?y r3\I'/n/s"Y\ ۚmژz Z =_M9r;TTٮgOnnt?8 KG2/=Xa/'PU.S{iɴqߍ*4W*[+ߚOM( T[@=%s\h؁<8mϫ/75 ?K`ww#U۴,9*צФH>'J(3qv=P&xQ,T:E}Π \<]97QBB-x?Xj5JvtRWyF3c*k6f3]z5$pFVJREЮ& qēnQ t>X<٤ B#嗓 z{C129r#86;,m626x0ZX~o Tn Tg)#{N)-M {ޢHg2QͦK65O`ueQC,^UX&nT{H-X8kS7fpd` ݉¼ Gޯ<y`m Q96`&g+zT4 uVO#'QJZjyXVK ""+x9>HЁI q6rORӑ29Ñ 9_roIt>{o-P9 k\r v<HJ)ëǭo`M!ʛۢ;N_/|I#t軒FjK 'Kl%QΜ6hMݶVro%%|+@n^&djM ǎ?.,l^o+ -*7ӱ*L0v|dsz;M!xNƀ|P '"&} ?u&_|>%3n;܃Ozr̐4^_G;ƂdF 8 چ8\iPt@;uЫ!i5X#ĴaQcrtIP(R-!!xȍzbV x\BXX#!ir<,P~<:Y1+d1bHSfk_[JZ %,b3p@rr:J1Ԅωv^zcI$/(. >;ebubmwws̬j7ta")˜qTf^zNY]kY+ zKwz1':P~=Cw>+'j/aTlubN9i6?-ZӴV=p~ֱmmc&w}w563g0?fԩE<1~<ֲe b9G,;blgdO⚙,~.nkVe<:|@ټy-T˜VY5@VAG:}ҠI wGR;לu_y-l[vPaF.ӿ5k_g"иyOp%0v@ ? P |a4? lͧkX?IJAR(:{77mp#T[ˮʏIv;9lw|~Jqf!?~iI;T܇ CYUyۑ#[ 儤5/d~`$KO._qD>%AB:u*&ÐXo2[~=eCӣO]`֚ŌTU!2sH%%v¯o׳Q>9JbmX&ٲX޶mbݙVr޵/kxwl6|f|q7Jq`]\|Th-[+n]Ckhq<z?;~o``#Lk.~ :M>'\׌$]ɘ=+?r"~p3yxM~uhof/Fiq gsFX&\3|/9Ͷ cW^~6.qY7^ϧ?+w\u w@.'S7D:o}ool {˽<(_L˂C8>&;<9LUL],yhٲn'!-_տ9#Z #9a,'pḗ;I?fdB@نd\aSrVo@s`f=r5`Y\Bh,BqP" F۞fh6 i9yIt! g65SGZNp^`8]6݁;>"hwB{{)>G1%:P]vlܣqF{f?5G:!`P͠|& ZEEq;VN'۶Mز[b&[gqF眜<8n2MbWsev'k?˚>t\q,1o>F{-qN.3k!N_#o܂aDى;طsbg!m(o-SXˇ G `׃6fzM-uP(MB6)Ng2?'w 7j2RC7JXXM$l S%H +61&s,9_5ihtpKoǨQGk'3'$VS3[-V%A4TQyz&źakk X5XSP(:lvHj楞хŹBP$Ö`(лD8hmb o*%q:'P8X`3䥉8믦i͖J)ettP{ s8c|\"?养fP+ |uݫ+MP("6 G ׎&ܞm/\uXo_H O}bjo P@(Լ l6ˊū &~_S]Xy3?#+)~d̚1 w*qsL,kk祖ϫ yY[/QY[T]7V-sBc-?'5˚9`ėF6"m?{O^B'U!+]Wdv) E{!J+]m}Yck.آ" ^ZbUdcCӶqN"rp  9?){׻E PDu5Ccݒwx'8&#'r ウF>Z؋f\سg2:&F ٗ݁#!;`<ޯ mĦ0.K7QM#39o !2)CBq8oҕP(a0 JPieLVƨ-oGupQVw_ؗyi _8ȴ!Vs+ .V0"2lٔ$ $AKbu'˱GXKwf7N鿟@ů*N{"9*oHCe Z@Ye=םAVP;|~xf5 x[R$%^FF63Gho;ͧM !WCNSBv&wY1PS(Ӳi$.>Pt&6jk~Nж](eusV:LIv VM1i|o{< A# |kxn‘SD(t*Rr¿iX^P L=zu ^\wzYj?>^SRIw[h@=!ߪn <`=\x|pwa&zBsB?z^T(Ӳi 7Q(d-ߩ1{K(eu-ߩ w0/"lZIGIv+GFs'bp!lw&Ls25D4ky"bI[vrq)]V )S@q L⒙|5g>;S2m7?`}@~־!KϰL\ei.aֆ}PtN9eRzd&v(Py#KnHMխ Gw­~yuؽkj>%|E[**U[=#ݓ8UY~1![Xy{bl S^^QMPDmI?H/J$)]d1RJ*`%{ J^kT6@(O/#~X n&xq8 bVi)i#.Ʋ?=6ח2}U8羃}R*G# C5KVy .%cNNw5o{✙g1jܴn3< \n(k?TMnᯫk˻]hyz05\vOD⃧_=6 dH ҁΤڻ?ᶿⴟ\I"Y>;D&=񘍔ދk}N1}osOc`R1' fﳥ NM o??q׷ @|_dS2.Ѻ\ IDAT0_"-3>+S"\^Sؾ};#&aTDXw_m"'W_a=ܝ5]ov}4-393?2)W[:\{W5E,'|v$nщX!|a hIƚjjL'6㈶W( }N`Ŗ=Gֆ?_cڈN~*ƷN9f|[&5Bׅ&ݤ7q߇l [-Lt M5495"7?>Sql'٘R`h+! 5v_Fgû>gUۑYYp fv$-4at^E'숞 $}Ϛ%[p9.fz @Z!&ۃ Xq k&b2 ޻m;Zo7x]]O6dL`$׶]㍭;۾;0,_g}x|Vߘ@ۃ]Sns}۶ĤQX )-0p{zlq=1mn~Fn< k;&>d ;xm=L /i oʕ5'{%$(zpUdY%w=YʰU|m},ߞb1Kc3Z t o._;Sjyej3&[N$})tqsol~5?=)5Ŗ='+GR w-3w^LBw>Yϵ@ vΝ_CY"d:|et<-#= z?քҙ3s aM>cB+x{ۂ3IqHjּ}/-֐0"s4 ?}IՃ#qrjC_ԶjkJvgg_ԄO=/zc'gw-9BԦz:|c@VG$ɼk==1apSgdmdmfxk 2xS&}yŵL 2s "U|'y?^s#[KN,<!c3 qUV)s$ց0H|}tO8ȅUG*^O'eR(KJ)ůe[,ٙ]}ot=CL*jDtmdU@e̶ӹjXiC%]mQPD,5؉B8,k'93?H_@V|z Hz}Kkpw+nd KePe'ղUud&d? ?pH.ڗ4C gj(+CHG66FisLiBfX/79CG36@p@oR|u%ZjIg?}LI>Na56e{8 WOP*6ot=ɓHSO(Ȧ$3)?h*h5-*J^y"k0 ā;3eR~{5ق }G%u57yi8#o$_t cҺ|1{ġ+XP_ C"Fdиu tܽG2؋FVq1i/f y3Rľ&Š1ɱ*kmӫ{&2!m{/yXlrQA\۶#&mM` >4-Y DuX ZQnR AScVXT37Lo1n`C! nb\t-Vs;x.,7U<{WHZr d Nbp`*P(P( 0gW4 ]- )iU$ac?S%Gq}j+Rn^b2 lWa>XerXsdE8ZPXP(#[+ >ߨ@+D4om-".VLe0|A>(9.9_=ІKsBpR ;XX^j2uH yf~-63Я{x(_V(Js +vt@⨦ QlYtXn6 AGcА64-RUa,i+%c7j֒:6tGYm_oQV(*8AP({`2%~nMs 7-)ku56ԧ\ot=r!NXÐ\S#Et\xGQɣQVyz~%fUV( BqLSn EwE$\MӑKqҀ.%s!NP融V^!7}66!8F݁Ff(w^j6[ őޭ `uJ*ܩknn5mCCDi7r* Ҏ|q†4496U_$.^r1B>J+ E'ROTg^M/kɭB7M[9m^5tXay 6d/cz5FjmMiM^)t$YeOdͮγ4+}`) E'dPtObAY&BBH$BӑDcYf䧆-Ȃ IDATN۰ܧq_ Za'Oo.ut(m`  `)%,-3^y7]c\O]nuT(}t)ݖQь Z$Hٜ ib!X[1$cs*dXVNʨu]ψ|_KgMk֮Ѹ_MhqDkot:*ZwEYd:4ը^mw_orxX)GEKMvIf Әpd QRN]P|]Rѱ_!&%N0Z 1 ֹOl m33A<83H_u0V]?_PQnSY^``.K|m aT8LТ w>/' E['g吠8am'A0֚E0fu)쨖Ɋ>y>2uij04+"XWHh1n"FFz:.u!j?+ u6{3cFcŪ쪖+ёo?seZ/J/NUo{Μyk3//INBъN~Sr*}&vgzu^:σ:7KB_J*_WtpjoI ]+$)52ؼB+:nV'34 664dMe[-%{%QqK{fi;:g۞Bw_{O#{xo.tQ! Fy5xl9yFMySeK lVll,hqMnB#h vVЈxL,gBAְˎZ/;j=vOy>[ܚ.M7.۳]Q7q~ǭ X̛֕!W cIAaysWO?NHL0„IJUL)XJxN]ա~0%i8dxlg |Uv ={搦oZCFMdڤp w`ƫWWdknA0e!any50KVj[3<Ǎ68@1%Wyc$:W,\lk` M'kNn uN o~e6{5M0C %&lIv8q0-KLVDTgh+"\9\uEw-E#c  ,Xb~YKuFe FL>^'BNF ٴ"1^NȔ:M!lX 2B]1L!7༡Koj9NP^%shEahz /Tٻ%&//HgYKqS ΐd6k-b^\1LlA/&:8),v؀qaӠsH5K'Mװ-?\A~o)4$כ&8xǦcb$քiqq5e&/s0=:L&)-1yuŞ+8n7B.-Vh|p1;4"z4]lF6m8+LqV82Jf1ܒ誹m47+lcD']MGA$5 ek! o.9H5P'_Z݁7T̽j<Qۮ&.oi>?}l B4r]rypF~o ?FsMaζq9YT"96 o/C&z͜3AZ>}nY9̬5;op,{=Q=_O [g?^eѳŭ:cwDbf~!}}Q˗OKCצMiUciTӖ!N{W_Hş3d'/ryhWkdzhr`z̕$TQ^<FwU;d4Cn*..)״t?-|x:9%Jn^tj\Mb\ z?mU*safL*_;f =_e'L[W!ފn^6(8B./x5?o׋1li:QSg0`),5(*HB\7#A:O܌~Ӣ6M?3CltdD'[t(7DvEܾ%kVj꼷(-+0g (dשq1Kfc5zn# R*)yTXT#WY5J|kIüZ@7=Cc66? 2@Yy-L9>G6G"_n[E0j>a[CGN:O?H>*%,ho/BIBn\9KČR=N\3kdI dֹoԬs{Y\wxzG~]Lu(:瘾W\=0#+盡xywֽ<-,;J*wԤp|t6k$JG̘æ7Jj[ ;@M'G݋!tP@ "v|)m>" 7+':xX,rg:e'WV'?XݩJ*18TYYbЩJc\/\mo >"drCK٬N<_E-֙Ӡx?tH##Ovr TCq֙FRb'=LB+=(KptN vUBO/} [XJGS7aJۻS8a9 /YU)-R- 5Z%h8Ʉ&ḛ+d: X-Ko* lB@`Okx`8.tȊ_779U*7dx4V x%2ӥrEtA\on_=K{39W|) 2Jj{pad]Hn)22pÕIɨiF0),!Xr`#.$)i )pIF&3ya>qm\?rB&M>Z[Q'r֗[\P'\}ܟs\U/q;A{)gS 'Sx܎fW^eDnl)57#>aP}>޽8r-x3,Qq_ž <6t 7ĊQP /W .-~mlږ{0T׺;RC5:d}@p6rܱ\u2:;Bܿ#ӽBmy9jT`79dH*VQOX>ƨorƫ2TTK.UOEU WAq5-[!GKj,N?7~jPqjsc8̣/y=9}[`ZF B$ƉCU f5@@r z [Hو{jR+(e,dFƬRʘR> de+UלS]9a_"yh 쬰vwU:,?Š'S{ mcGn锆t\ `ۧNi耊U5f=1WߑF褑x'oEAJng,'`_q/G/B07 :!G_ 4dm;j\F_*GM+ċzG\:ʊ2{*=ƍc@ ? 9Yj!Bl]#-C?TJEZFw u7Hq ` p9N!o;_~l Bu5R$ ڵlly(tpyEY .K^Pqgq74v.2CzUǪdUZ ]DuZ–[Cuq8RN#O4ov͌+v6} p hSdwDz@Rxr:jaЄq+wYWdnں E8Fd?HLRR M0XAU SabO7r d'гB^U5@HxGR5 AR2sM5gaH|w) F㜜W(p P5Aaw!):QYs#*]XLEct E1eM45U4SifB,K4Khrh+"%ƴXGnP`&> mԩlب=H%Wkձ-tÐɿ%<~$i@ 7Դl޽"N8Og%t2v̽1`ɜxӳG(b^L/|}&N8'^7u!v +7lwhTI)ѥ9-=^Zװ^mP"hM=rx?+CnjeLLY.!^ZsCFƗEu.V~3N!WjMRƗ*fIXC\p?h:]V%ykX$;xɷ}a69]<bY90+ C!Ia9iJ )%_B'/Jo}89&7Čb]7<]8-kBl:?oRse 趺0z~}'nv~#KT) 0 /R{z . syN^$ fU%2_=oaDL:ݒ04ص.c_ IDATߤR;tr(l Lid-u#5+3ߵ>ĒNZP_$!ަrenI4fߵ4>G .Z)8hr$V덺 a^ԯ>]r 'XZG&3]₤ 2x A\e+geע2ȍ'HG1*0݋*+Frp͗0i^*_%ø3' np],좛6mk>~_#tt޸ YJJ;ka̫{-u߰>5\dSF8Y(*}tιF9;µˏneb:Ů1),.{ôϏW䌛7Uw g39ڦeڵ o#wy\#qyNr1z^CR^gZ@0TjmPBIZF(ബh$5aP#E~nj(:bH{JnSUPd6EMFCd,@ Jʃ ҥ@~(E/}Įe~\nA",Nt@0?}9LGHkI1|Bdy"鼄NA"p) pÑ]-H6;ɟ5 fwMFk,7s|O=ؐnH65WUNmmԜ-9QF6MK:6\5[?k2/663Z7l#y8֨!a/ڽ~H]}fʺLbF nIhd6JE$,?Gx5icnt8>ѷgU}PYʚ5{IsW4K*v 7mEHh9֠Zi]'ٟ*CF]t$%&+ .Mp1BR˚Q*4 T`ÌgE'\XI=ܒKIc4 ̇5$;1)\eXR4zԍ]si[X\ږj|*dMV2Vg6sY= :U|_#|)#"Iq^hk$œ,:kM"Y#?7[~L2{]񺼲MnyzBZ$[_LsYO[fKb3Xvu6.~VN4umuѩk>뙿`8@ٶ9TKD-X2EE H;`c\v|>'^E](-aŧoßw q|\,غeeٽş F vNjO/_/m8_1{S5PuM|KX&dRz]rj+is_2"mδn9NXEpcYoNJ%6ȪSzKGccD EaJѷ'i Қ}/O~= (dcsx5zWϬY?]z* DB5R z{7/z qkCK+&OwP:ndޅq5GM]Ǒ3u&R* ґw>A~Ǹ|ƃPqg\@**N5d29W ?T:7]3\iI/^k/_LNÿy[mlڒÉk s QG3}6 ck~k\pb1- ώJɘ.G6FBAkHfB Pqg6>WuWύ:2' .2BÛ7>Em *< %Ef[AkIU6j9rlq8 ϸnnGzR)gaf|ذٝ-.ӈtRPO&;[Iޚl>-V´-D-~mlwngጸo7ȴ7\_ G)}",%zc" Fגd3-hH/ S؇0#%3h˄)ƐDEXzKPXcoѕRF\-=(zG'QpCO2uf_vDp״=sKf\'nZ--eRte )Gc}K˼K`7cHp@ һBa_uЍ.R1uFN]llO4[x6V<]nx%̺nM`+ݩX'[Y|H^5&vc[ aK8b IUݠZQ{gGֶ,Q3K0\jK@qI5PیT6 Qцa$ c$-cI9ƷY&2 l$eHz4B}4& `΂aJʛtL1ٞmllZf 4PsyEoװ8S*E4&~1IXՆǭyhr !#WPh3fƜqw)Ͷ]RP -W5~vƤUqav!S705|JOOX鈚DxWEZ$Hwi#N!9:~?Eap#561شGg^IYY9/];+ ,TXP8+B)o# K)66ύ&Ë_7 H:6*91BG:4ZhҔ,]V41*( ;#%~Wroo6=>[4:edBm&!",B7F"P{J}Y,q66ZTw\02qۥϬG#k;|?i\6d -ixiJ">@cj?•:zXzMP4g0wEeAgTuoFЧX**:Zg c4rwxXұ >'üZ@8Dx N1*[P/̻t+rrnn:0j E" M À.}_!WJV Izm|5i=5ƦuzMo1VƃT}*ь͊kDpda؍Y|#1֘xyFE l*/ۛi䔫.DmW3t=]f)(+|zC=A_iw *OSs31ԩngW]t : =%U%AV *f(@c ;}$p .r"XA 7.ȩ @5z2#C:cNTEMҸ<^F!2oBz+e@Nv9a6Vgx4s<}'8d]s6؀yx N$ _)_uz% #eg}E8R7}/u#~2v#:6Ap]3v٨?wn/VEŇ4*.m>#wJ~^^ƶi4/.mFÐ$[ ˂0&h:&jL=F ?2h?=jCG㜬 :qqGKzՕA dX' ^GGԙ=3S;G5`eVf/ <4 zkBI7 tU$w@Ide?d~Cg yTz8|0{7Ҹd'3y2ol uhVww{WJXo)yt8Wc"#ɫEZsuf",E3B+J Q3! DRlpt~M+lccc]!d ,\6xg w<#܈;ĨF Q{kwW2ڳ:y[t5|sq ?>'E?_/NJG^>d39&2"i}vڅ73:uRFor_YO rTN'(j.llllB 0 P(D $B!s47wџI*M4tQ|Ut޽NƦAZF k@yO2E0=_qP[Ge{ߪеK.`3f*Lz~[QNA4s4tt"<Ȟ[ؤ~ii*gڭ8$qy3ב{I lZRmVVwhҨK$ a) 4M#rvPvi%E(:tf>(u:?@@AFLF,H\pF\G2E iH[m+M{Uޛ8WʑO|cF-Y\t\Pj$TWOH:7]ƩR#L6n]jKnzؑ(6361L]] D0t¨h9Z"jѝdxCGGEKn6oa Q >j X7NM}~BOzn+OjHOTΔuº@Uȵ3Q' KMG #o6ԟM{Zl~Mi5 tu9{=x\Nx66666UUIO%## _`JB2$H1ZƦ*C|]mCә {#. LnhBGdGWI5lE[2 dZ_~p~?67U:FS~0*7f2b-?< op(UԎӍCؿ{N )]qS짞fpljyZ2oȰzc:±<4yzY.Yb8CYˣ|AkK^zk ,38CVaxoG֏~%JÄ*]ƞ˭ "WBkuċ.ʡY԰ϱxW|=c& |rJހ 翹k)(z#gӮ?2+˳)63g]:bOswl˟شT +Wḵ NSA_n,|8zaMh8Ma _QƞrI^O,"='(B⫪%D|w&4&(~RFi:Ʀ {#4OVm-!'汇]Iulll iGaI)c1n%/6666-AUrLbF½6vMpNƦnsrra0رceee(l dMys!@ 77^zzjky歷fܭI摙 Ka PcZB IDAT?8^y UǩMej64zr%wfoco=qG"H+n`>sQc[_=JsiG$rP\_#  :>뇧kE\^W F&ǝgw& abrg_nqڨnZlsn>K(qnoҒMh'#RWF=:,T~llllZ*de"u#b V"moalcc2Uݑ1ևHKOٍbp"[+DPjbE|u!EsXjJBu5T^ik5Uut\ F8DX8pFA(ٯTEdlg+A4\*$+ҡpljy3iac^0~]tDЅpI>4=j=Qj7= z^Iou5iix5p] U!n42O=LPp:Iɺ*I'c f׮]x3sĬ_1wgfҫE0BбCƦq`iaQ+_@}Ut;;']|4 T'N2lJ4fDs8vBLj8<Z YqoEs0Pp8덝-tp7;N$ YXI5i;A# TY?~WhefTTW/lOOf%9@d`RXLͷI czt '+6666m@C]jT!呤@FsZ1KƦa !df%J @PpaߔR_x6Dbo Hq;df$0cDHO0B {d|60˘7TnG#*%lڣiAE-֘Hr邁 ,T(:WWZiI|T2mi& Dc|-φa`:F MnvV:|WW졂tMw: mll"D&i**9TV(B*^V7hEH)$Wh!  % ,i.9 4&W^شGllcc2m.2\!ZH0mFST:w2OO`v;'8>[fx߹llll]•lZ2e=IZF6ɧqY^ d EL!@,>ԿJ= fp{嵒wWyUҚ-S4I-66 [|'$ XQ̑놁de!/ey_pA?rQYCn֎=3Hǖbccc)^~-Ǽij:zF)K?5E5;vS^Y a*TWWBZJZXC!oDl)5llc>MAZ!ӡqĖ!PAV^N$n UOGVR1a]lVg6P9]S j5:bٽEl7>^v0IqpjpWщi"*m!~/6xaAEM{ηiw$G~Hfp9jk.&$t>.–= KF_x6666GǸ xry7 %~=>V=7[FsbTUr'-dYnϔJ]ne}+1kw-mll=XJpA('N Iwh[7md߰ߠջEJ}niQ7z))KxQTZ4MCԮgQpe7q1w5]ڲyʃm3t^gKʒ9sk*zn͛8kII.y[[슓ZOS_B:ּ8jc:ƃOxaIYt8\/7 ,6666665եL!Y!VY'cqҳguA?xYy" )FO] d"ot&߷e*f6r:+񧚣4-`8r>!?@zӑ:A@("RH:mciiSXp3b$ͼ;k;󙟳ު}_3%l=>cU5ԳIn9N܋-$[ۿe٥Cid w;5,i17DN~}L1Kn|![+%B8;1csH*̼Nle[EC7pvɧ %_ 69+JMÙӃ%+y]TzXs$NщT6vӱs5;wdx~9_YmUv"8TsX/@"2au #]} ЊY"UIat}d零om<_qv9&'KX< @ jv!F lb+H#(ϺHwoZ12pY#5Al޾L<ٯI P(ZDWlؾqDE( {qH[40}(}au(PP>:'`s#5 wb0+8\LHCR;ӛ6㏨:a} Hj,\̾ \0+!|:E:1eDBTMSrYj1X/zܴ:E|8?LFCt ŋ`zvDORjtٙKx]``mdϊ/ p֥Vjٱ#~Չسv3\64UUpd@5SzJU^/4 Y?XǦS ٳbeN aNC_5y`r:v-_ä>l UE"W6e[tpC pXUUsIcK=%G>oijkum8K%d5;t,q=Ɉm᳿N޴xWE}'s=W-}hTyF1I'YWk 3]&%covIBuLhqt#2p92$5y|%|sC^T(uLvAykDvOuvwqVTw~93w }ҔŮĒDML3ych׼1ɛ) &Pt1m,y>ݽwi3oCg` O`ώch$њ .oG(GHB"TJ1 d˾ЃG)nu[U&ZRīEk-hz{kv"ZZFpwb +b,8VRʧbv!_/x9ߥG"B{xGō_6?_Uc}o΅a `8z$<<ʗ1ؓ3f-[¢XTtJ$u^dGAHA"PM"&M<!H $ƋX).&h18b(nI|`[}샥˸ ˾5xa]p8lO=3Jlc |',#:Tf۫Xs<ܦߘi8"ٲy^ÊS JCik0%]-"p5%R+hjdwGpe:#2˺KݷC%y^hdݮCKΨyxGh.!![oc63.r5:%\RJqZYUH}ewָ6Zhi /%6;|AMmi¥%x= ~:ZJi(^Wؤhm逢rJCGJN8!ON)Ϟh]μC0&5|h0UtXS!Eؔ ullGWqD30[T)MpBd"Oq` Ӱv)k#8T*-yw谤bPk7avsfh)ACX)N:ӆ{#Jq336Eƥ,\2ʢtdN25E]ȩY5".?)±\:.9{N/Iʅ0r[挑pbe܈2:-7^Ϯ1!9 wZ{{4Dχ{jH-iJ4FK^n=13s),x9_!j{ϗҀētS;bd=3Xl{3WNX7/nIiu ~G|gf9}n|gy4H>rRcpݝϱKqujIyƯ=wLMxk)Ϲ9_WmjJ'yd. ]4Ospk@ l/%2צ=NIиit;M%5hTS[}JʋPŃZcM4o;?ξA([7 lkOqp IlBA| ʆ1uP2r0f5j/Asc. fնG55j@3rh+h{Y 8Al7lh̞yUŐ{־ɎK6ރ`x(cIce=Ǻ$͕ןM&J 0cI L̼/`K-Kyr[k^/Oo,`Sr+ ,e+WҞ Zٰ[n}b}~x%@/D>;.~UxCO~̡=,xtΝIol "=x_":l JQ~[?tӎ ~aۋuDGaT q7m~f^Y"ɌPu2ڙ'+Y\1%H֊P8LH+ECv`O%HqrǝcU"^1g\p9&c$ќq$!bK?>0A=`:\(P*;ӸbX(+!;T3.cl:A"壜0N`ƨ3dD:I2 ^<$0-3B_& "Dı?_c_d(yo{PfW2K&H$}Tnᨢh7|Tw2J]q6_[UأU;㎩&*|^-4\zp&S@x k!}Xky M?˟9e44tбv>_KHE9Ǜ' korݤĈ|>ҥ#zUcgFk? ? h12˸?<ƣfs3ϲoVOM1`d%΂o:7q<3R"׶QT<[~z2_Ӻ Go[6ËKsxuBZ8b_@"uӓV\S?&-;S‰ly*z=gvh75@xdt=!Ѽƶ`LzREUDqiY];ZZa濟Ev߬77/}HJeP;%~;k ѳlME[CO=#H5`}u1wKE`XE$gR,8(>{E ߅_oe!͞Eܿ|&b?{{sfp(/w}U[KЭ3۹gIe'S|+WO.)jo GvbEe{n*ޮ!d|/xv.jc͓)ÑNkۼf=}՛w/>ާǻ~Wߟ ?!D.L@NXž%Ƽ/(e |%ơ7y?c3hTW9LnoǏvb)t*A-?w?Vi6q\AwG, iM.X 8mpE *i{2>|6?+kj6xf4:W}흔a5+HZiذu|[&ӿ|3wr##~o,h@}AoO!vz4ƷI [W3Ig,qO8kiolKޞdO\?2f^s9_?ίpǙ]_;^{I| ^EiY\1%Ljv2< Ad93>v ,y!,/.=;Pһ~Ӿ8~y:v.ft8ڳ]pppǗ{n`l]pd"+9g IDAT]ǧ9Z.)џkK&(-gʧXyLn~i:̸PCйS?%ȷcy|$|W]b8V'[NĈy6y8 OE8~z+]>3!Sq]EQ.Л#ZZn5|}_k||}%%#m>{^?>yӫb¤ILUJ۫XQo7fVߌ`0ͬ[\kv-FE% ?93FPyjD2Ɏ]{|)%R*HBJ ٿZ2du0ݢ} v4x+lLټ4[{/%W}g=+A{ ӿ]lRK"qH%Rh;'I"bM*":M<"C|]#L~}-I*l+C 駉'=&Ks%aʙV6TTR\$*pS66NlC n*৓}J:t$ե&ء!ǑES;᏾*mZִCs|ᜡm7ޞ’>]yBnp1g/$cV))~FJwĎ,wgWKqrvGsڦ`013/֙p+wI;;bn&BEN6͍/̬}in dgA9:zw/@ gA=YQF tEAtJ7k(od݋eת.N)eDN QZݏҮ0=e/(V(IɪANPUmw:H> 6R'0ʂU+3|}Jޞ%Ƽ⣣ׇY\vwj"T {b/b F=~FElz/ק;Jwz=<7?WtFzwl2j?5GZl0ے5u&fA!@AIQ5RԻ0nMh|?#xv>rDF2oZ^!;l+ъm A陣yGiV>{qm ;lN, o͉Rܹb &ٰ u3K6YL9y<,a-3C'MdtT"Ә.^=V53x0B{FDvmf g0Zmr({yd !RHhjm==F5 Z[I$SAN鏺q$s&s&Yd L'^Po(D).fĈGwTl7\WSSx(SN=զ@SbvK'|Kvi5#*`0dU9^`H)5ZNQ$rb Z_!R_`m.BMES,6{df)-`gs>v1}RuoE>:R݅T AdSbiNؔtMiE( +͸Ϭu0 S.A Anʆfl:` UCS3Tiy3Qn{s!E&[4uhSg|E> usQ\C'ѼM .Q>o}>@m-eԡ.[Vfk:8_ꫬ9W l~%6рپz=0b(eY286O$ihlާ\/y446O$RAr\Q vgڌ9[5g? #V.'>O;>$$}ҔsG>ΈNba>_a·g3% u1'߾v<Si@'fg^/,4pǫ)>5 )@XX"Z*y):)#c Oue&R`0-Q10}(@f R @XG@|peT߀#|7+lBZ3KG{ BEp:}IiumJKRܛxIK#\\B_]|ecb F)2bQ4jfЅ{4:<ShkR*JpLh`D2ܚu%QYsg>Cu9x;oL ?ޭ\`0/p4 >)d.MTOCS3ŔhoO"vTF뫂|Re֬+sl'符3`K`$g% \UZ)3ByhZ HHSK+ED#(`0y8xi) 2ݬsF ˜iUk0 # ^CV|,VX70AF ցS>BCS3!&8!PPFO`.(}\}R4Td2E< N#)so7~Ul=9#*U '7a`+h*kMNݔiǗ6X(9ALimkGe>p$|OY˯ q4S.+Z"Ń{J%cW)ϴm,[.ofpδ಼1rMICfjjrwL8F2}[`(6$ge+R7V}(FIGgJM)|gJ ͦM>Oe- ,Z% Vc%^Ba[}Hrtk^٠Io鲴dX2X>,^j)?<N,l:Hb8I55oz}+cvjdZ.iqV͢Sg1*4kz,ئIw}̊$Mf8,Y6gdɌ~,Y屴bqY?ޟfi %3Hj'V>^1 UxeO Yo/ORbv Syv3̘(l[>e,ίThͪU vW\4Rzꎞ#O?s@%%* R߬o/ǻvmb# ފQo ^E)J,8PlrǔJ̼V29-- +wBZVpR#2G)Ee5%J\y6ƅ(ktbq!JI߾VHqW!ZO+~mKQ16#{%"3^-v C|(y,53Z7H!>(+TVpߤDDC\3ˡ:%XK8 %N lR -KfsYjs_:9u;|\5}-J| E2J"m>sMZ-إBxuF|}c>1ۡ&-9_0­'$;e.pMM:K|qM[`cfEQk{XG[v,i;Lu\13sCqv3DeեoZT1R"l>yuGZ-!C\U3ƅ(XԷKUfsNE8sqYE)2L?"ņHO\ave9m ')ْ qn WY%{B,Vmf)?9~Y2)KeI[K`mTJ[)%sHS.¯ȬtY E_# ކ KYd^5!%'g]`+#$Je"F k%ѧG5Hd;٣UMW8̞yBZ!WBZXDI5T+.,z _ k1>>Tvu:r2g>cB\R}>K.trÂg<; &"bpy>ݠ9v=BY*?$d^F F{Mm])>דOQ֣t PR#Sn3<a<\4Jj`jOv8>.o$nf4o۶*{45laݗ|tI|n!. 㙶/5(7+̷S,(8a=~/I%!bI~5vyBq oy#[XVYk`#;R>w<:qyŒK*Y²9e5e?xXٳ \I) !}J-OŁ,X KJ(.b{Y_ӸhY'5C` x.g;ASAc<٪RU îYl!& kΏ3^{X+NZଝ2:lEF`9mqV %;kt&``0: }PϙCfyV*f} )|sǴPtpdaq`eY4msi*\Srj R)"FHP !])- cneh^ R:h(3 pH!fP+1abl*Dz(rHeW]f3 KYL%HQR EQ5-k,=*JiI5KM!>~ͤiX5E\#8f`IzmMSⳡPo&6M]2SdWIA4ovh)myalpJ ;a<~֨)%9\?4G*]8rM|;vN 1gmc:m7g,wB), Hd/a>v͈bɫ^ ַ[ ܠ@ڂуeTx:ixIZ$Z4۲}%-E4䡤gp!:ȯBfM eM#_7ofAɊb`+) ~?!!`?%&\,mʹ4ذ]4P9RilKP\Տ5U8L]O͙IO K\21m"T ߳BK@ Q )D K,h;JBR 4K&I'TB(+\W[PJ!D' .+hU6MK0f/.hK(%5BncR,\Y<#DX$ɝK.)hg6d$m)Ճ_ҐIQ̱ ! \p,ZDQVPV\Rbnf.;8l]ixq4'ϑ )|HIE41[3'Vnq`3g9 PB:#TV#l2zlb/o(ʥB`< M|R(e絇YJ{w>('B' pWA6\^u-Y=1 # C/%7nB0yg.6ګ/r+tb$=IRҚǒg9c'M()){C8l @*AEđ"# $:NGJ夔hT@̆Gv漒)#{Q 5Mlj)T@(AqHB2sRN{4e|!PRATpdɆRf DE )O JX{O%A-;W6ܳ)E16wpvJ!{`o;I [BQA}Bu3R U綻5?"3*[є_UU$^sL)b%ZBb~ ]'`'.(PHAV-J4;&\uI&"s2ϘAa):#@`NɀREnHbI R2R|qqN+xn3y|r.}JV$E L'!5jُn C2 6esRv=)d'm[?ß ٳ46Ĉ%鈥imk#֑-NeI oU 7a(:\,1[7 IC9iX=%DR>$SYj18 խI[RRYmQ&>3.--BRQPW6UV0Eej f(p~B S6 [Xf-ZRasQi%d_ BоLe㒤RK$;&̥CŅltp/ep~m-Wp,xi_Py+C̙lcx^ht)$!G1fEQuppsfP{A7/nwiee u=$]Aq#Bs9x)zBl2hdE6f9.}8D'Lw#db"x/-XO3i |~ eO57c4W57p&80h]DK2A*b;EO(lҖKok#m;xG4c`mxG|/}~d;F)ŕ3ɰ $4;׺|{@ W7ώ2\=V95/O$> wYBsa~7ZӲOD&h8%_;/gҚ/X*'Po1xvyE˂-.Y ɌݰeS#̛ \QS;B#L%!{ȱ&aY(3Y_<񝚧)n.ʵIqˊ[.r1:4ԶH9%D¾f"f5Ύ0,'ngI‚ &dhS?{+dF8k:r뵩׹TKy b:ׯHɕй\Ky/&\eѯgޮ#i)Ͷ |np>S@ѝ>p4``p907bn+u*|ܴ H#$@*EUYY)bmiI⾇Nz-HJaohvl5{IBKO=E$\-~o-LDU6 1YSՂ)nG w"+sz0>L| D_- Uxrp4a`pS$=cڅE9lp$BJ\C[6)ҾCG}AY*SdUŜ0q OXz 0xsh`8y_dVoTٴgKOk\ ,*G2dDc3 G+ X/g(풊7)(IA͒Ri3f[6 /|VْǍ"jL8-H!5BHϟnj'3g΅3GM\6aoc9"܁3 T^6{޾"fOf| 5+N7h Cx6y OEH-XXQm /AH-HL:n"VdJ1hIP^^Κ5>ۦoU5-}'4ʪn};(_ޢ>(s{`x?xO>Ͼ=-zS+]gqx>f3i0:Ӿ}9-;VS ˆhj;Ё>˷Bj;<4%eDŽCQ3?ǎ%҅/jX0x@,F%ʮX>+ޙ `ޮI3 C Ok.C>dO/Y,Οhܖ Cm+@(-tEXtCreation TimeWed 22 May 2019 10:23:39 AM CSTL  IDATxwxe3sZNK/$AiZW]lZv׵u- @QtҥJfWs]$9sfpr~s?E%7:#yΣJ,B!B‚T6JB!B[hHB!B[6B!B!ġ X!B!D X!B!D X!B!D X!B!D X!B!D X!B!D X!B!Dh! ɨOdC Pi~4s{< àW gʕqB!IEE^_Dz-kRB!\sUwªU(/CxCr;n L|`O!8~?&'>>Gsׄ=~<-`!HINfp˸[, ۶ȭB!01Ll0gg%xGΦ`FLܯ}KB!d..:m w&<=zPQ^Τwo ]Hq8,Z_Hee%.=~}">>u/sѩcGsl/*ʫwiD6X6m*'sΡk.$&&RZZʗ_};&B!Dc`]wsׄ޻b`YINv6}Ͻ"/fbzNB#Gmm-'ARRÇ #55ښZJJJvn✳ϢKXźuСӟ\JJJ{ڵ+sΥ7~ naСjj23駟Q[[GVzL2ŋf=lڶiC֭ٴ Xb݅lgÆ|2grTTVViJqq1_̘(T!0̡W^dgeѳgO J 6nd]ww !q?sҨQ4jWLB07[%l矛5k=J^2 !LEEw}.7%zIB!Y~]ta! p ۷+.sp83Ι;+.晙dge0?~.;vdYHb}ɗoGQYV0˰m=.+O [+pRa)q.i}B!BPRP};=Nzǘ| %F(mѦpc1PhcqDYg]Դ,B!5 q*+bWΉ=h b( 6>V,tRn|x" ,6aG +oB\ U+B 4r}}JHhB!@,aO7^R1*ܦIkq(e`)'[b@W@)0+SEe^GIY o B!8`nbߦ:wwfAmPAq PBi(`Yh6CIuyq( 0 0ڦj|85V6קv b4UB!,ajL3[Ii}.acD\@iZa؀)))š Z|\?& I Ɠs3SiyͤfڵsB!8%QB}y뽏Y2Nj NN]H Rbx1a&,LP`ׅt~P6J}^3ivCrB!BL(ؾx w2q0xb"B1S",q(peڢ*Q{”t &5qq>Vb6ab@C 9 00Qp(sGb(M&w҃xB!ⷯ n~7zGXy|<{Pr\%B1]~URC|,ְ PtV6XeYN'LnDtA4aŵeQUY sc/԰aDls1dŷYp0ձ|_94!p/f.!x鿭_(ѶlK(.) JJVvIIIJ.E5(x`lИ"E\>p8S^r&}:ƻ0AјD)ֆxmFnB!Ğ|4u*„* >u&Ӕ+Nuǟ?K8@jk&?y1Q|o_}0#aM,Zt\aac_A}OT;85UD8MibAo.i`uA&'`EQ*KReK7i6͜E!'itv] O|OMc7 R gMEcD!bocށvp)~5bz]x#W $q D 6{JZ{bfrV^ϖС-f?2*h$36TJklw91e+7RőDjJ i)itJGm@Ov%99 b}IAGrf>`Rfi$gT@ˉ rp/ya9;\g=~K|]~ j?7)E_j->_s>!B45s4.93in#W:3zxݱC_˺!$Ov%k쭊 X5,݉pTySѺu#reE"jXhZv:@iЊ޵.vG$.'_B4FFD.$kl"[K(*.%lEi׺[rx)<`ѮO:i.-篡 q6m4ogb1hbUԆpMurcUUؑ-%W|d17R?l˳wk-3ֲxtw\Ԍ^xWgPNOƋo'[Wy\6:>}ٓz^xB!8P4+ç }ulYg'o}RAӿZeݼ+@b.:6tH]ɼQ޽=eq溊~ǖ!urX1̥;31[rl#돘b3tZnʰsT|:\Fs~$:jcnT-Eqpyl*V|G`e (9wr\14:#I;o=tb޺X㡼U[+蜞 h:qq|wDjc$ԅPTVQsh-ְx=3[M4jabvUxv&W[W>.Qsţ<7V&v;ԋxblnNk M:~EeoEtoK) \'TMZ%ͥ`f3V-5k⯗.{47|?£vňhM霕 Jc(0"]r1m Jq'Olތ E8FiGcIUgZ$M%RFUml`qZjBըxڟk?ĸ!A~8v*_LZ]{ ҭ!8v&'XM6Yg_c u@gNN-Ҡg˨3ˣڈB]~DCaxkUq4/Ke>}cMlinS%=PŶrl ~֘`klCmEbϫ z zd ؚ [a3|'ĥ{Z5"dY= \d%1'&MepTť.Mp3:5DK犱SwԪ,C}U[TFqeС[޽+_|g /.Luq :K!s1|`At `$1xT͓0qLy4ڲ5|t8ίc,/ķIu,.Bzp8O5ٳVat͙]0)Y8OsnYh"Ғl@\Q]D[JM$"0^QmYd$hI V~ MRfd"l(¡TVbYԎ2, j4& &Wq%wk)gɭ˔gcHoe`YjU.ڊX"cKW&lH'r |S\p; tfIC8fd&9{!/'gB!8R(;ۏG|hS\8֧wtyC/. $Ҫ.9σ/`Y|;!8TT@#x糾&w*e1˖͠v$8PRH 8N mf͆Jcf$[Ʋu.ӎPSk[igW¥)IB^E+YImH4_f9`Eu_0jh yk **%ᢼ ۋBЀVL(-+ɔWTO ԏLem]GNiD X&(&2"rܑLi:p"20}d-H˩}SxwFjZCvIr C8It:T/8iXA]J(A恺uYdV$nbXv9@B!8,RFs;Kb2}e>?k9!خZ";Hl?mT)o0&G2G*;&w< t}W^T#ɼ>@`o:v$RlLC,j6"7A\6$:fy.,:f4Y_wiWSnz R D}16 tPS"dmk*ʫ GwK^4];+AI\,?:iZp`:]8]at:V^IX QRvfZg̝cnCk(i'u& 8;p|#mq?2ѯw3xٹtL\.1?ZɶެpХ_QnB!84e+>w'tyu^+x|C9eP(LL<\kr8l*u$v QǃSYtj_ IDAT䞌9v[N@AhBCD}Yh%WN<//}(xf6ɿ~&g;'/0h5rM }ΉżԹab:{0#?D9|ɭ<|_A281B!wVF >Mby<~#%峷oa8թJ-s8Ec,ZjL3j( ʟLh#_0뫼k(eԿ5:fqeh1krrm}ިv:*+$)Q-3 ftᙷdd BeyHNX USVFp5ljbuUVEq}=o,,$Pmb2{=M?>YVf/mcY6sB!  }ulB]WL~o_u9n:OCYC&1lACc)Mae9ZdAP֚![4Ƣ-Xi#n107 wF&?#!9TN '@'𿷋 aL}B-eҭ;HVRB!aK1݈_QBL2 qɥJJuZԄ#Ls5Xeܥ۷ z+/aQ6( ŋW kP/d\Q[0mN}ƹB!ⷡ/X.-2lhQ?)H% aeB0̚h:mނ&{G Cu8K\^F$_K-X#w7ڌxB!BD !=n7-ڠ5TU"F7t_Ь)֕\NJ*H8*WSѶmۄB!J+5sZnZ 娱gm<.ڝ~ /s&2B!m>BƚH`u>jwD7yKV>662 |?u!dzxs>)|4"+-,^H|Ba,>aДUTarr^=c_AZ>5v3B!D Ǝ1Qhlf[``ԏà6-ixI bʲH8#f6 /&02o'3SB!8p$ qKH{I$w hEˋm^9 w$pԢG)X,eY8ƶb&(Á2,--Aƒܘ.B!uX,HI^'C?m[h(b{y) j6Fk]h+`_#M$qT3cS8K_ x[a QkC3WB!8̻nkѹmwSښ8oBM8DPPrP SZU b@$eqyD2Z'ECv !B!`!`k-'wΝϚ)غ mj"x=% LnQ"Eh7%$RB!`!~"0/pR+UTRY^i(4O1]jHBƛOB!8$ !`!Bq2HB!B! B!B! B!B! B!B! B!B! B!B! B!B! B!B! B!B! B!B! B!B!Gc7@t[rJKJD`7v? BqDۿ7rIJN"!!A>R>,FkM:nB!fWJaN oW~7eMӊ7vB!(θ^J,h<.˅ֺU B!#b~Rj`!D1Mu;nB!ؿT}h,hTJ]a LįQ,É1BQӅck+J6q95W-=Lme_n:y^^;nDhŖ0DS _>^ @R!`!D-71܋te<< ۦO 9Ǡ]oqKX>|%/CGtaVz5cqY?kT_r:[Mds}~v~LSUqmh_sv5֕"d) !G_b<Xҭ~6iSg|D !*>3(Y=kB;~V9xjvΑg֢2t!ݙ|/鄻qůps(?;[y0qm'6/ny`!B@%杒 I4)ϢK@$'\̲%p @g"aoRBAg2ТojV0ucy7}=`oYܭamfɬjs7 oGl+|-wc\Q)_9Ͽ J 2g ϧ/x9l#Q֜~ɐa6-SS!@Zc7uw4>]*g*}F7ά'Zn*N熡v9Zמ̢>||um`WaʛɊR.9tNl?ǪL|q)'?m\X\8$Ә"h:];x~\h}(.>@~= rI.>~a36rD ȅ-W_=oM[]΢O&Wk)ssԉgrl|5K * ^6mg^i@eSmݸA4o0B4Q$jm9\jjwㆤܹ QY]K]l7˲F1{c=޷c;D목 ېЋn?%) ƦXS#>cR>m0n՛>sĈGӹb_n_u *گ>Bk' ?䲱xre=L`Dxpl9ҧma`D~/;[˺U[P>%!>H]{C87OO.ٶ݂ytR!(+{jKč8yv:r<M@< |{tqޞjp syWyN}{62;^ʸoԲx,OͨN.i-n[P?q oߙ=RݜxD5TUR~+&Ѧu~Mժoū\~[5M4޹{*W^ˮeڽr7'a˗9#ʆk;#Cޘ·MeN'M¯B&G6V+{/M n<c%98}v>Dw'~;<ݴ;L;%<+óxcIG߆#|R>1^ʙclByWUضZ৆%S^ELFINs;<`H!0{WQvz",؝9܅E`ϽCE8.ڊb~z <ʫW5t> oNe!1l«>X'νx԰͊ik:w}'DRg7'']/zvk |ғ+9;2ŵGxfǜ9e̷ 9B,po=q2$ >9\JJ(iy3N8xS_5q3aEX=T5Ḷ2꼇xytT-]gx︆߉0 rv _|KZ_ϙI\CءgwTnxgoB?獻cr|~WN@ZPYŴ9;*_ЅYkiwAGUkY>DziA܊m%'s1_׎G%l=Y?HJ)z=~NHNbQ 1{FVTҁa̳U,YVDQ踳:NKXX=ֶ{v4͓DНA ?1O bPKnm ~ׯ]1Oj?ꟼ[ac$[툪(nև~N/)r˸Qzk+ұteilLfإY%B!ġw@@+eŋ\uC\9a.2r2 Zl/*&H">ZKUԠyd2g_=amtǣFpveT8 @C۞Ӈ"a0(20LWݞcEؼy;U&]3/Id>NJf\"]!d_;Bٳ'9%滙|Q>( Zݗffy.j΀iP:F(l{w*D&5wy߅U[CV".g_*۾~ȉ7Ϥc/n'N8wp ;!w I!+ ߏI+>/?MPq~7ۢ&՛i`-^H ~4CW,g<B4 -As|/ s$$ƓணZ/֡R:27L_1v$eEUwo?8 5L$a?Z_WUCv+oVv?tk|/kݗ F_J!~~ )3}2!lq4C0c=9&WHHOf[1Up)/.%԰bЌLO9 kÎi5 7$3uSyjܚ5@ē#Gvz6 t`9}W<٣MQ7u=݃[QO'Ih|M7T4[\s}-|l/^2_ukM`7yCp e(U'R޻3ޯ%xfM62sh&F}F4L:ͩ? ?e[U쯇$#?z|_c·=0Ju)]CoQ?E+2%cCS.5q+UK\LHس3/ufAAwTEűc,Ϩ|*Xn9%ЩZ )D$Uusf!FC->Vw^'UiS o(׆irq/ܽ0 ^FUő@t'n~N 1(&o|LZ& bHy ?{r,txW>JuKCfU{*q)(&O|MyrX(ZMEr6,N Ή-94 T2NV^3Tuذ%kI# 3~^FDiwZtYPvD&2d ¿Rt| }%UCMAںV$f6&ŁZ]a!!ņYhcS5 B/b+Z]fˁjO#>ʼn-˄!,q31dX$bU \пҶqADl2bнAA+`Ag%t;AAzXs v6N   "A)fGtAAxD,Xl:  D,/faA  +%`AEQIڰ;x`AAWE,Ɲ 8 )iV^O'GAhAq8R,VҬ611  K  ^B:AH%=.zhAAxD,/ z z?AA9[,DBARP)/UҠ"ۂ %Zh@u  +-eI8q"N$]z ,@E"iPР`Xq=WAy%%- Yu cngN4 E ?Jt{AAx^z,cG39(  6dՁSҡ o1贸WA{i`{Y|d ZՊEeAo% YAoRKhT+ ~`CڞkGΔX"Iz5Axt:XWAo¥̳EW1ɤ,`ԟ NIZmb`ԩT U&4zgȂN YWA%/Y?u>6HZ z 8la˫źqٞ?O'GxH~v=K%)3.f=IɺsYu:M@Uӓ^/k ֤xbS xjS`*'=F K;'ʑcӐ #XDZgnj(VWp]Sxw;CGMWp)*N\d-z}`q-W'J>6@rsgP,tc~UیPާ[N`gl'#", gǬ2ʢ5ns $gwYV#*7?s4EJ~ Kh8Đ4`۔|2O'E$Gp>JOB>/TFjziz5¯?/xD]3\WNI'V_.LB7yܞ3L`|v.ك/,+}0U!(2(ui{%Gp>@B/iE+1jWcr2fES [9>&|+ y3(Zgf޵09EK%yeڹxRT#E3oV\-=?쏏ۓPjG4ϿD%dʖ&gC6p?G]cӼMDWBcw?ĵd+4oZ0fvNoeI~nQ3 .VnT\| R~kZcIک3Hhrcp4-\"~'HsmwR);*5g۪~qA^)e[T ˥DqlJ~S8Gy߁5Cg ћ^ݧFjwluȲ))v*{Oڨ7 ?w6g&mr5er},FumxO),S>ZJShT?KŽ yy]fc#o_Ujgrk̩$*vg 7ofTE#Ig3ݺl;}zq7s\L@PYDm]˺cwza J8oy*7M,?Qt׳r2س!ųxA\;^:7b/aqMrf$ȡKY=*5,wdsu@'S;GYzoPFm["mV݋I'ʅaK(U1s14WشbCp)X^]R~Tn]'Y:5!aD٘5:miX|'GBJޮ2y2l\̍#NzHPI։ݬqK ѣ[zfs;%)ҪokX.5ݳ)ҿS 6]9P{?y[s=͍5ӯEYr}s"̶Y?d|>/$]al_Ѭ IM齃wgl? 0V* '2*wnghN,sD4֙Ec8e ^]Ӣȣ-IZmj U o4 ;N$ :Lz^4[X|"%Vim*;کH{2{d.Bp玴(b8;Ƕ2I%k-HMWݣYz.'|z#? %ɳ"V# qw1g~B])Z{7-#ZMsɦ!ZƄk)Ԡ݃L8CXb#;CѦs7/$s!w?o 7Y S{#P暈b^t Z@/Km̖/kֲTR@y:ukE\Wl;=/՚""s!Ҫ0s0}$oX~` a?d5!gY6mcqS]G:y4Ev[L6lg{Ѳ#+BïzZw캚p z|P+r)̓)xrP{Ѥo d3ȮL|ɥ$]֧@b(WmbXTOۺșMS=Kofoa,JӮiS evM$vtnZ 5A*~=U-Zԧjv'-[Ofj]†#׉v)V5x~Ab9}+\#3JVMZ*)7pu[&=:7Wvw]S6~XU[uwOa':d6tD.ehۥ r'wgmjU͊B:G#ƴpv-[_;1: ҿأ9q5+$5/ZvkhUp>qs|j)O|_Ǝv+Ba){ƌ?6Ǵ, lfwl@/ qyuDjv,J70sdozv~ ww2yEr̪)|>;&u,Bު[7?z\(ѨCcP@< .WͲ)ɈY r^L\Šb@vz I|S"Cz1j ߠ7iƉéS-Xן {ۈ=ٿ4rv0~HQ4~}Ԭis3rqζxTHEշpY8FT(ZڳKț." ʘ P,DždlqY8o+} D\p2)9Chώ3icf_PX>\. l®_9fϓ9<oie*3t!{O`G\>&LEO)T·叩tyw\ @2txzf@R̡}؜8ɵ5l ΛJR Ǧ~? w)T)mߏt}{!l$rbzp:zî<4laٔKipj[4`#ᾁ O ~k%!6w9D7ww<ݍ57:1UJT.O1aՏw~kn)\dtF3 =Z}<˽̉-+Yyœ9!Vϙ[]q{xPFOI\`Ge(sLfov\̨?l)֑-<#I_Rr`72"+K2yK+v7#{ 3/PA^$ٔ蕈m S/(S4d,ҸtY}Ӫv?4/_F["1^VHg4Xc;…i|sH2yH7|F7%Jpfttɍ?Hb { ôȩ۶ gtX߈m n+I;&\d;gDXOd͗<ʽs% J,dc^p5H)gM|Tf1H)>-ɹV(3VGdzu/k΢+\P~:ք]5,㤦 5r ̞Vʠq`Di2y]#]_È-xzak~?+"X~ϼ5,|e\t/g blߓPq|Ǚe֢ZX6'sT5Ƹo*^ejS\K4Qu|2s`=}r ,lQ\&F-.8K2iqtSv9OM,$IՉ--s _toYvy5?Jo_Hϼ27Wq#z.pT[' o; Ȧ=KL]E{7+[SgPQ7aQt[9ͼ2YƟ dB)ZWgm׻/ z ~W>g4 ~8ƹFxI4}ѩ|:EL-Q~\5|H=9G1̣S9weP`TٛW#}]Vhi= 4887cvz`ՔKCiKw̏Ư1ӗ5M 4ySqԼ#m,ssIoX9g#)Ϳ`pzpK?݄1BY4]'tDF@m'rI|2mY?;M؈㜈F캤*I,9u1ui#/pgwcfz,M3Yu:ixYyPKO긦ccKMJ%BUs-+SoHoeLƤKD RH[꬛ܓR:@mD70a^VleL>QOk 4},Mꊞ|%&lM;5K8"k9Chőw R]VތhV,UݍWl#(Rz4;3z XKД{k6GI.M<}yj0_Pzh9:Ja?1k>@v~*+AZ!Cysk"5f@*{^^nڇ@@Kj ܓMŽÆ(mʟ#Gpol%^kѸ[f%I;&4HuGw G'~&ƌ%P T C-٢kŢyoQ bʈRtט>Ʉ;-\<3A7YUqRϏh(+/+5T*4 \$܇cF+$g!ы|wټx75aČkݦ,c{ g_<ڮ )6^j (7q..m1Ў~ƙ+ͥ 'FCi/.SXf}e<{äm7;3vƯK[ϬHq6H+cȟeNH(41bI۫PzP9l~NHDFsLc%{NзuS^WjE*eZgN f{^t?eKfLP^?q{ ?Q91vB耾}3s^gO۱4ϪwMށC8M?c(NFѴhO]t[c͇S |0wWu=AQcb#:3Yc fpJ4;'Bt,@G:ƣ簮շyGVϲџQU1>qb,Yu|sq/J͙35Z,SANIXpt/v?ffI8 $(Xc$IB@Gߴl=~JՈG Dy KAwC*Py4%͆:)ӦQHJd|^Ѓ.JƢԯŊS$oc jEި2yT蔍iIiP387zOrΕ9y'Ҳ3nZ؝썩C(~qsO~q^\y2gIF,a8rE%hXlS:31:u7S?O)~R!@ͻ3 )w.yB#H TlOz\&]%LhƋ)Ȯ\ 磽RHsG FA7Ԉ9GhD q 9?mtd>ehPR7WnD^@BJޙuIW, $raqN_'w6'4TX Qeg8R>8\-==GaR5}T|K&s)'U%mM(aFHONB|pO$I ];R?/fBDC6ʽ +y*ϳUGxC%<2C1 kWo&C9PU)HcRR^,/r=-F 4v{ӧNܟOycQf<¶0).EX!/F +>Cʕ)>WkaxS=3JK2`Z&\Ͱ+(c KWċRrf,L,|hpL~~:'$Qv!F8 ܹH|$)g+d MpVIDnw>USjcMTb27Z,joo폷BU0N]wR_..Ū]x z shM^jUŷLl6c2}p"(U=jLaC9{+;h ;k~C0, IDATf% 3Jir5,ZR3ASeM{gT.G}9a9~뉙Uzom>&>fSHKJ~xkՁDcУE`41Uس~}noӺRX#w1.ˤLωKعv5,df@Ouذ**u1 [-XU.éסIxMt=@E犛`ǙEQٜh]-"pHNj0?#%-F˙ërS}8d)75 Œd NVyӻQ!>8)$t4"I࡮XHJuhגR3ڔ2iպaީ84,KSH贚JP;jbU]pR⑌xȾ$Q(朌CHOOK\6zfCk` ݎ's:쩖' jSq1VQlyB8#dB1uSʼ%t}MaǦjWs\=}4H\8prZDbFliWxPEkj?Hh a08lX0ͳ PH{δRU^dž=E9Hq`ɸYz g*77S])\'ӖՈY3߿d ]77͆Clx6{UdBbg?d\\ʧR/,c^ ^|HWO9ѸF4PA򘤇;iMxPvڰ:ktxxqrGK3'gұy[Z|{GiՉ0gVt?C f 8SSHS$'ћݞz!f7L>t}ې&]PXq7??9|ʯ"7Tko=sMR3e`W?Y ܅(_GΓؿK3T;cUpڱ:uɸ,)dӖ$4tYNQcstv'yE&ZwccU^MG>yw<{ xYO2 ZS %ثR{#mʃ;E៲cWRi] U >9v9l#H(O3'.P#yd< lDݺK*yK E\KT)_>U'{l;侾ofvv َx"%Sdo';/H\fR& 'DӛnehЋ[ hǁ博dNAF_rҸp;'~jE.À7_{|9=Ys/`nVg?-h==h_ I܎LcoDDf * #糠uzá|ϒ4F1˕d*`|';'\vwFõix$hYZ!!^.ǫ&5҂?$Ly2dLU ?CF3nuCj'9[ fTqV,.WU,ё!wƥONї\.KE0wu҇:)7W_1qErg qb&0׿yc|O?o?sa( ubUϿ'+mObI3776Q]sj)R`.fÒpwGiZ7Hf,ՊnW9FLJ*I\:Ѯt ;Nfٻ''u,[# {W$:줥Z`%؋'onKo3nwK_ο"M]cݞp^u4o,}1ͬoFӧe^O;}й hчW0}D2MP:~s+]c_lBpQTPt|:^{nQi@ZTf}Y(N;)WMXu8|6+/%aw:HΩOEf>^'ɡtXzBAkĤSb!,]v{nv7RTTK 4%Ss*%vr/#)GӲ ,]8'jlh]1`IXl_L6ʉ2/uj~bkh:{aUApuB,@c f3.ܵġxQyU3h<T񧙷M F5ܻv8\:QUnUmy.AX :V]LƉBխLrBRP#vSb܌ܺOܻ̞caٶT{BY%~9Bx5F($RZRmӘw&%s>FfqS8kנBÄ Z3^.= K=I7Èr9ѩ̮s|#`z(*8^ 8~[38QaӸiD-EaɆ\Mp`KװQ*E2ydž#+qIAB&7d9_.;O݉a#> TosBzT[=^-fw#;׸>[F;% W%B?kp5i⪅;> pqR6YPT'7PSC٬'5:VPk1"MqѠ<uzPV)+u0`/c]ίr`Z;ӯѱB&Åת#d ]!aʅ0p!g.e_ċ[G4OT伵T1 Ė'}hMHBn$(a]]C c_3m\~U5}쮙hD{<1d3vGe_á}!Dfz]ELAth3+W;*|^ NN 0XUGqf/=B>f삛Q35 Z"ؾt+_QVv5.r-E)ZS*`뤪":sFR _j e ҽO)Gpr۟q׮pe, a}hF5M7R| ӨU! y><7sz6]S*_K/ /UOGkv\KeĶNҗˬIDr䛌9M#ͺ1cx@_!c|0fe1歏FSgNP](}hP|>25I?m|ͼ'0E.Dyg zNCfwMQwdoWKc%nYt"+Trw9$z4[3S5=ZdSvOnSy*M87MA{RQ;ߪÃC~"Iz< q4zGaHsV(>c>Μ[5d_3G6'Uz3y6rܥ6s>jƾ^iUo eUEKzE8a'sb7y6w1^תU#{}MJٜ@T>^eSϸ{]4+&%1sŵ (xҤ[yN|ڕVvݿ$҈98#nhTj;>@$!S}r- d/LHO(FvpաR oQi^v=4㧰/y,Q4Tq"KP"5OK-hȴApB!IЯFё.^455{LXjؑp#q*C)usxNƒNQ[,ݛ2YN[ ..c`AS+>|prHct8(ۉ ØѺNa|QLk+3QӲ!bR-0tз>{ ]: ыE=$9Krԡ_5 A.fj|0ZÄ{ l0ehvy0TĿHI3 BЩ)Es3./t|݌O'n3=ME.=OÒLbl C֧=idO,\ 95D'4|(T`p`,dRMfLpZK0yc2MQ&&a՚0QQ*&cHU. ?#%VQr&H \ZE hDzɒo(v,vuK*Uܼ1wS)PsP(A2< B^[!)͍1(4ŇW)a?L%c$$QWNRR2d{Q]9Q 4^\ՓNB5$GNA{aF/6.2gd6~;%9rvSDZ* G+ǯ}&W% G՛ErN}Ȓ %=%)reSYP,B [~Y$x0\Evh "f,_9]$:R׏f(wP`:Of)Y>$Kw?/xb;))Yh[Y#=5 b-r~>')i.XຣINB !jȾ6`RxIKg"nه_2bKW&-#5AÜ=ooPkLVe>h)T<ɤVBlU~NJ,a2SvDGff~#Al] ]$f=$@WP}.XCB([R2I͒1ك0\<œARKP(A撎mMsYA)HDH*_hk~'int!U{fb,p%Q|x}c' <,S4z2oV,%ؤ?٤DYMόz=7 PT/.;#R%^XOjGkW(np[ ,w*]Mœٸd23!#a*E89ѷĪ:SXZl[$IBҲӟ]2?ex|n; {SHwe7_0H}ZL_QYbh =j2% >;ݴusp(,l6oOZyy>]B9̑/Eur .ݼ Iݿ|!<+OLQL|)pa@5^8Q}xnU4vY3w}Iɔ59%+% 벼F#iWl% M(fѴlRboOѣeMZ>aiVDO`n\GNcՕ?Ɍ C"A^zY|~O۔\ǡ3X0•P3/r쒛J5X vc T%#N^@m\a;9@,SbV)b}aԊ .|38~O%p iqkȼ1+z >f0N|IPݮ<8+uUҎcM &&TNt/|fyol}'X6o1O߃I(G[%oeηkq *!\vCHӿ_؟@tL]`@q|z$Ԣ7 oCT޾UI=6/Q#zSK7J]}+4f{YCWb|b-U3-XqhY_+>e0:EpU?I@T3@Jn"M$.S M˒%{1Q-s0YKo`*ЖAya/WgsLjP ຎ"/U2P~)[2nRG|m_ŗ+pN UogB>1ty_ٔIvIj1;qZ;UѺo7B5]䣬^_$iUԭ rD#i;]懌&L 5^:BԧG^tA]*tZx|MlA5(t:Teh:"Z)cOChi%#{q:]x>Eq@) >$/42?([bP4Y$ k MT{$pN>NnŪTdwl_ DXӮt >O7ql$Ѕ7w=7/E"cޘ?}ɤ*PA𹩩J㥡(R8*S3̌C$)COXt&ͺˏ |yʸ]N>бWU Mrwh2,odUUPT t&l3VKoBCty_*{r_p{m /o:Mj;̊4o%1_H@#ak<=zmheϧqϛ[I|]s͘T0;C.5&|&3^Ĝk ~Ui"/1ĔН}gZB۱o}bVҬ_B#_ @'?_.lihuF<{[K9Okг?{~!ƛ]sAhu8C%^:§oRD=F:魩6&*q1_I/Eͨ0 ,`#i}wS*^fQ|o1i/KVVr)mj5yF9>'Qf9Dzaֹ 4<&Nzթm̙MXw֟=rS[͢pmSgۣpiA2Ѥuc*ĴQw^sGhsyW6`ϊ5t{`r 9O_znq|j&\=Gy4? _ 3f^p=S*¥-8r(C穟Ŝeel^ߖq >K7pԗC.b/jR7MM-ӟeĻ;(vSXFS;||yYɱɐLh[@3z|=ƴɲNbW'Vr֜t,eɒl93oAŠO7e; qǿ~H`;|fnIa>/pQ."q?c֤og䰽:_ߓˈ+ߘέj`ʪe5A5t(-q@8\JlCjZE2M. L]1a9_.e,Ѓoz<خ"&@N…ّG4t;օa圩5)ݫYA)W2z-MؕtɲOޅh4Ȭl s[zv6_pWIƲ㰛k+]ڱ_X>c:lY;UM$&OhͰ~fj4`ǜuiqij{%P "JyL-|4OgyT7W`-3gsLkY>t2gG<.5OG[SQɮqspŗ,V{!2s.{4$یnCwV M)V}o192%_ZSFD>םI^{c5}xft6+Zq: Љm-sv^½xPI$7ӛ$ߐ6sW0\?agW:0TzfQ_6L(:]*eI4˹@ŭo/5_߾ 0?=y }X5c%~/ٖ8mEtɿԏ9)W=L^܄7J_ j_>a Ҿ/[5Dz_4{.֌C^d)3|aͻ3AXfی0^y^"z߿8E7^5;/z ƶfBe=s_GHl_219^>Ipzi7Co4KW[8nK;_|bj¹&XgRFd'}ږpFa1f6F0|~ }Ǐ ?='+F>Enے?]k6' k>dcy폩dVXNz#yw\Aj<맿_c'u;y}?oֵM>iX^hs0:pq/uvy瞞K 4TKN:W_Fϯ`Zm+1,1o+Z]""!ɐ?U#er0\Y ӷVqmR9|9[YzO_Ħ!Xł]{h[ R0v%1_!BsEO0V_eзFV> +bor6s@$H۽_ݍys8q N7Dw+P6nTňVbS\/ w~ܷ}7+ uT =jKOI=îq~bܜM;J R[͇tu$\0t!.{/xLVRtj/[uKB% jNݩ]ފˊ Xw GzRM/!In[B5PyUXӵbc.)[?6r F{U@pSY4 {k,W5JѢz)=NL78h1gCW7B 2vFT.ݦaNI<2wFY_ϊl/Ǻ֦ pfIcKK6$o$)cq<ߨb<6\cVZ5u73og01L䳸6<; 7t/ >`C(Rp ~Qۺ1 {>Zef"V(i#A4scpj jyx[h~nC$j0g%tq(v4ɻ@9Vgw5Z47qfdqm98Y8u6[֊cYh)rׯd{Y6@aY7+wgе]Y'I\݌*zD\ @·<߱̉iqWPey>=$v`Ubt*?N?{RgסukaDVԓfs*Ad )|sڶgHL"^R1نVGeSԣM]Z%$. iyI0"dܥMdH^5#3pyUmHc lAKfmaܰiC^MjGFwK%ڈXKVd$U;Sk]rr 2Rɐ X6aXBÈ)C0h2kPɺ#'¢Az>`s3a)7$:*|ٗ,uə$44J-|r?TeTZyڵsAʥ?an:~ wg,aQm7-4\7;K@v8/#v287 6ZL5xUb뺽>J#=#}gRך[ WQo0JF$-J$`|+89+7fxmň*M+I5YBd:[:+WF:INv㵆q}h;>F bo$.wޛkc/WM=-eXM~fT)2u~)d8"ޙr3Pm_j^뎱Tu=ZrFhOZ Y0" =vWZ\:-C@9: 󝏘H Aw;S0ĤXܡ#u52z}$)g\Sܡ|̯$ Kf4~4k APBޣ\j£aff@M3Rqc%Hx4/`sD)2S3p+Iuɀ"7Iӛ^ M_*h‘ɔo1ft@@P o8U)w]:H9\-\ Qx9`X0wa4Zi+FRV>zM$׌g^~):*X0OOjl8O^ȹd^8ʤ٥`"9BjRݑY`z\Bq Q&b +0Zk: MßMЧeujԱr- c/ᖧpjVl%Ef%\+Akhi!f)m6%MofaS@:Efo,_3.LU4ë_OS`ytwtΜK#vt_sjnWg:J<^Ee{ZFzݎk]uBIڤq9]x%FC: @:jjJ]숍LOkICq~@3-ëh/x?6wYS>PС>5jDMnדa5V|)& efnN@q,^Ց?oO" %SoXCBLq @;awS1H&2)i!2}Dd6Οͼ=u Ou~`{Gp -;4N<@-ŗK9->\YBNߙycan)[ ȶ`6t2Z3QA E8ޥ&/)~?f6d؆M&eL*kJeɀ(_V<]k{er "i$O#I]Nt-Eq3Hd浄,G%j}yf\$c8rtb^;0φP>r=6mOY~*CaцO=@#c$ w4lHq9^NHB`/~ TRp\;!72"?]Nj}욶䵬}7hCPT\tA x0RqR)Zz>Tዷƹ/1aQU~w:#,q{l|ͥXLۤd!n"3L3t::$ݏĵ|5ts+>.]/5.[ĖѬ9@TT߬sl.*&D'xg(j %n411e+`$:Sy ;nƕD~NnZ6<44:WÜs +fIҍ'ǶFھ]W)TC%dTKS'ظ~4wܜEJif=ÎSSf>wM \XN6r5nTc>huh]&d-:׉=Qa*$gɣ<HӆaQ]JL'x<:=F"uG-`R]$BY#K$ȥJ _"0ʘ,ߓ9\Hvm9GӐ3pvXrO13&UU_{6bS]T7~<PqtxtN^'xrNT}Όqes7pY_:HDJy{)\N S^p;ȀaK;IT@_b͜\jғ.9YQӊ$ȀykHhҍ%ǼHW5T_?~=GEڗ[Ո@ǂelz6|[35ދȈ;WT6$;ԳHQTYFd uO=BݯT4YTyԤw"}7ûƜ[Ն՛T9gL0h@u :z\R$, FVׅK .^ɥϊnՅ|6'xQT3}pNuHȦCxtu0`Kfn>DFִ* ѐ^-l__h 2o+'cnhN-ߝ(Dh')ߣ-~bvn`7F_o%"ԊAG'A_s3-v AeTfr`Y2MR˖C7'4Y'nRbw B*Uw5{sׅפ^%6o>Ii2YAфRu fUlOސCCL,z@sa/{޸LFLrpf:~85s4oN7+ WeCF h NRВ҂84VZ$ v=I>,^;D5>Iui[ kDٿ)uR&9}M & PH90 .+Fz|)> c\ ruJD-Og%'t?ɈȲ_O'o%r˛g^f(}*gG n^Dwʠw7(~2-)U8tC~/ZɌ4{U^^0nOv=To <^oMY4,Yx>gKN7=G~Zxr`=`4ӌ܆ʃ޸Nu8<Ϝ~ ֝j7 'x~]Фx='DtHnxd^5&0nѹ[_wgwWW'qWe}Nq)vCNx5 Z ,WBW}/6arDM!o*9w_I͝+p+Q: eh6d z%*_ _ND}h05ԝ6/tŔ[NXH&}Eiuc"g |0E~Ci?u{ee$ˡY<= IDAT@ڍJcӭs_u'Pwoo0y9!}/a#dw4 ]Q\y!,K>[HJ)<и;aѳ{D6Ƙͩ\L3q弱Rc4*.:雉3=S?'XŠCdvQ{no71u8j'Y*|>u[*1NT3,t SW w\ɛ&d4c̽d~<~0 Jdhsl3\asi~W !O@P~ϧ7RIe6Of# $^ƫ0A񒖒HÊm@c idl(%ZdY.LDݔI#ROVIb#"RrnşS~e6f[dd*&IMBn3> 4ŇWc6c"ŏG0 ʣJ%%a aҁ&) >?ތQ^22h;Uix,ʩC |^Հ%Wlt`"TƜqff7bRDžG@`J9IJu!ق 4Weo>l(IF񒚚l &<(:L&}eii$;U,  WeP8|3J޶JM&]Nǘ>2RqD8e/=v~ eQ*.$-ÉlJHLPX< y GANS1UңGɗ?UI/I:C)MǀIrźM ̣)xY1ce?iƼed? Qva)*zC:UƧ`ywgTL"d\YNdc v͝ANz5YF)kf<>u|9wwѭuCЯصpkT\6JD>0K>T/],نìCeVe|@ /ꍅ|gwr/t/YN/Ɋb@ٷ2rʙwj ^jDRR}dfy鈭dHffkqLj# 'ө` Nd#;3Zo^AO# lv[f c2/qFZHNY4ֿ6)xd0^N\/yx &g؏Q罊kk-P:^L} >uiQУHCABEҲͮ)IMңCtzc5$pfYuCʚ,鍅l0cSp{^?._2`W!KJ?hPQzKt>eݣ'1u7\ _/JP n΃Ӛ_u bU)ޟ$]0ٙGvb݇c:֘ls Fc3UMeז q1h$`x/eb9mK&1t#MM6 .F2b_fϿ`M6>|g]k LP#Kg9Ψ75_AnHΙ<:}'gN$,._An_6"nj ע)~|Kn*ƭfT7fl&]v-\x^0_}`wYグ7MBD p6$h``fGr>9 IT+ʸ^0 VGmEѴjv} pGe\2IPFo~#6< Dl4h@_`AA&MryQ x'eDo$8Pµe<ҏ. Y "A->"AAq{v\{ TUŸ  ~Yq] &dE`AAy}~n/E^KAۈ8=^TMb2X  4 ON粮%`Aیjo=exG $ p#|~o'*xAMy2~Yh4`20FXA M >/8W#`AۘioC}~ z=._I'$ p4 UT YQ%[I w$Y  CdAAA'E9An=gʟ]AAAAA'>   9Wb,g|ԩ(iTNBTp\9S]I#4*0c/d'OtKx a-TN7H`P(lrbI1Ga)<♋*T)[D   -vfu\X8ʜt`d-4>-.sqsoͤ# }sgI &wmfnC"AvN`@de0rt/Z]ɼϡكOpy,XsL.[H   #%"2[K?1۷S;O G*ц}چ: ۇb $֥PM}vzy PjQf~.JT1!}4E~f o:K~X/@Z=:R8ʕ M94D`AAAۑ>m]6k29b\d;25[0b(DQ`6c]dxס8 /had*O<)r>S.ή)_d!5skG-L   )k("MR{d'.=!R@RIY nސ n3! Ă v>vN=^AyḜX'±|j6*GҁcD8hY9VMAAA4â/tt =T)ݲ$#!|V Za;ZS9pO9iύm    FoĬW:ŏW5l?L 8A1d1edM3RaB""Uў:*lb bkl :G`L-3   LdFb+9L"TpwrΟIV)􀎠VKtl rqh4hJ&ԏ,&DU݇؀!:]ͪo   qԫAҾrk99* ٰ~3{rHPسCi5/PaS4aVIMAAA8M22f||tbȨlvF4 _a9zN ?0= O#^&7|/G36572"i]} 11 *gU2iUHY%EQNGWY wX?  wMcmB!{d)Ff y_qGqn/Fuƹg^Mo|{Cס3u.ջ0S[ՇgXj*@RFjtz%W ?p c$@%v忸؟ $9Ćhܩ9U z1tę$pKyWY{qGva%l=2Dp+Vvj:@ ʔFaBNxqobyyzzPWgh&L0a|qXWGQ;$kCayfuWmL)*Tt%;/irG(8NH%֌ ϼ*o v>zg韯4;.Jo={@0~![Qî`V|IiCݨ^7sT| U9[ ϮQؔ~Ä &L ^6e ] )[3` 8C -ן}ts,q.c2(3W_;k{sOTJ DF9>aUR#gs!/&~U_FMjb͢HNtQ_VAVߏqn]:z&ws/a„9+jaNOBo0a„ u'=O 69-g[dd_r/?Km6{|o,u:^ڃ&٦tb^x s`~~F*YCU ArRb;P@Q8EfP[bl{5P'枬h!KxW-?&L;8R*x`Nr0a„ s5=j¬^>ְaG%c FQ8ncɑsOpdtvT>b75 Xy Ց=؟a8F8 wOC(84uu6Fv!2ldPU:0$wDZ=ɛ8]<| U6%{ِc4芟*а%#wiII7ݰ=2|b0aœS n>~Ä &L 5^ia!Ia;0ala7L0a„9}4`U^U$̙n 𑒳{W\hԦ$v,mwެ|V~M¤qqkB?\·V|,WN>)_q'fcFIS%x(W9?A}:RIHlj:_&nQg0a„2)=7V)O?U]l 6[QvpI^%[W{oѠ: u^gHjLqsGpfw:_=|}Nr^y tտ[ѹ$=|?8\N2E~c<?y\a|m'L> 5UE=lօ6+a@7Yɽg0a„r8Rz"%A*$ɒmG|>f IDAT.6٪\5H0XrH6 AW PH, c%o49b6?^gp L,^lQ V3A%Y^w0X xYc4@QtA ֹBfvÓ{h Xl)$Ԙ$YѤ#bUnݍ@LgkGuIr,kSu/[B|7+F$̽\ /TQM VQ"BraGF9ג2Y\M۷E_DиqB{*5yuM[Hc5u囬鈎*׌o79j F ׹@Y`cyS[KI՘O!AڬcV0xFbG᪡*I~E6u /zN zjso0KL^m ͨ"7PcfBERTH+3q34sM8birmFI^6v[LH%6$;A~ j/ Ѹ/Lre*P?Æ\$so_"_Ƭ RetM A 7~sS_ص`]e X(So(Og .$ɒ66seŢ\j qټƸqtܽ&6?6tƨz *t% > FpdIE/9) ?OhQ h\Q)Xd{U0Xpj\U)XOHP4Mc\'A}{ۚf:;k.Z&7vm12H35ǂu9EO֙0!0Ag䵭v`R?Nœey1%.HMՙ2@!ޒcdzXKiA8^0^gX+{~ [!霪1 B׹Ł'50^*W􈀪"[M \ѻ䕽MfJʍL$s hQ FkIH$ɪC6VGʰ30%0$,Ä Sr7X{.Vؖk7_L6ʴ~4RP~搥0z? r+T:xmrWWAQ @czE2B誆EO0mJJ$55fPES$8^ $`fgM0\=dZѺ5HPț8B۝|W ωCZJ-y68=%VTS zux`:0m ug.W9 cXnNSQlyW_ !!Za=Ł+N^$VoWe!Q!Hp8ע,^ f|l_ͮ\3YN5(FTnpaPx"ס;]%PS<1QŎBIto Ycj')- _7`ģ łl*O]+vLV~\Smth?=l?H&=zwL)W_>|Ư:\ܒ}$];L턛)DBjmX `U."J,v\sJ\uTWl34.qE-ƎToK*3Ub*m !"'_-K|Br̦d&+ O1e_\(+C]b@H*S':0AEͱ:5t9MiƯrqGR ^i SJb$ $tODk|A˵%LlA@d,_k yghg)-_۶%cU3xTgNq[OЀwȚ)MZY4=>~?ʗ<;_gz2R7{I$ߩmbeKnYXVZ,->N2`aN3%B%ިsCſ B'8"IXr(L)qSmQ02NkC;G 6$K;uncCH. /5# @tW_\QVF ݢm~/& / 0\WMkts&jo&j|Ia<6=nr0oc4Fy|m/p07U=]"HꩰzW7F~Ft< IF,ʲ9V2,vjH^*IңbG.5zP M2"1 RY1y LJ#]GmԹd+>^) A.<7GtNunc6H,sj_/V6Y8KdEL ?NG[`0NMԸ}(zǟ3 W&sٖ+|= muwa'MA0}=WXXQK:ՒCłJkPqDr^D1m9?̗O`*w7}>֩6 _[-9Vv6޳TgC$V"vv%;g%DzɩҗsfR]:vjǍȧث۹;{w"F(*q$u@#T-4fG`LwDpQO ;:`ѝ`fߥ0ԫ8a~ /PBydJq gR8 T]oMiֳV 8;gEP'bSvEfȢXE$~ѰX3TL63KԦT/ٻn:^dlˑOb,Pkl T!Bؼft/v5Y`ձN uvIV"Ғ/v8# >;wO=(Shx-A8eAMi-ʔo뭓xU4#*$|ҏ|;kuw޼Z]2,֠(Aeyz71mҳ2IQn, 1*l(\$e;QYN]&m2+bloӓ.0VP4.c;mʒ6LUIq`$eH2[HuW(\y' ;'b5'Qc?RN) 3X_"!ٚPi3ByTnȃ=Y6%@ǶlD )/oYM t_m5dëbWeh7p#O#q {*DE&]deT^η0U.I|,XQqm.0yekz\$kE|6?l4~:dQi8*Hࠔk9|?ꌽ;ymg]֏Nicrx~{MUNpTNLΒ^Х&y! 59~̢|%3CLҋ%I)!m.z]N~I]+naSL+QrIɚZY@6;HfT~\!Mx>Ϣ }*3TA%! }&5!g s-/Tp3vy,^_ga[ ]V&[6m M)/,|!G[KLfF<_êN$H+Q[boc (#*3'yKMx@Lϕ_=>nU1AnUfw|njkP[BYrRӤ4Dgt4cA.@\6lv{4FvW8E1s M&+Ȟ ~&bIC3,%u~)PUZAJ>X_I*}m$Xf~ P!!U__I UZSEE@/% '/k+Y-,N4ȵqNV/U}ŮJoSS22>}?KqqVSAi>$u Ѷ쥜հ!vGG fl/dK.L F*%9IegUT- {ҹŴYM}5nX[ =&c(F%mL)PW ){ P)ߐо[d~'_~;6~~p8?Ķ$BUQqF(?=%i\c[-)Fv6 c3u"B~oO=5JDf̜E* WÖLk/R8.-jQZAtH۠@sq5kRYV[="U4_ƴa_ 9_ʜaM7Fƥێ8kj$F4Z|)yxʤ2M>+k=#6  < ۨWjL }@) ,^ тht+<6չ|7 .⁋U,4OyjKJ-k<4 A#AA#PZ"^ tA)Z1FKQQ')B׸PaL)^`VK-ZL|.hz *Z,T[s|bGK?Us*lTRtxqS+wFHJ p+|D&5U[ T.ARM6[==T&vd jIC[4PuAAEP>~IpN~:-7;J-2Xu Q>?MIAdxlq ":1*dIxmjc։N5ƗFaDNm2SKTKR%}'*g4TKKRT[6:D8>*7Z{! fOq0JV~ә[!$p,Z_+]\댯46d][#OAʶڋ⚮m,\d_/hHEFbaމ 4Y`vFﳾ"ogc?'ظp2nOIZxڑ{`bn7ֳ '3?==u6;37zyҘX72kIx+]pGeC0x%Z{ocV7 YSO+ؐSAU2 Gh*}'rό<~|3'^GZp$m|7|q۸gDl+YO_.[U_VMZB>^)M^̹s36k̫ŠiKהŴf&Y姱}7cǾ=%}O,ɠVefq^~VQ/?ǿWŏ:\|^_ۑČ[r}y(]]qpuB-(=bGqpEZTHA|'&߀ 4. , ژk[d q6T)KVlͰYi3cwLb)N-v=ҹ͛=AcpL?G,KE-ȩiCyĊh]w[ uT@k-D)eI jE_a:zLڢ$\3H!sߡ0cJqU LQeRl mV z' 2 "T 6TycGh,jEvl p-> *׌Thh >$\pϖZqS*d(&wo6XR'|A%>30;< iPiڬɴ9~Ob7.5;+mV_^3`uD "g*LbsJX*rL6QIܢZ ]3[$ظ/T'x~a dN<&k*~hS#u.x}`{,0#' pv՘lVPcբYI:gYV,bUnp<8. >lF@\CtftYqvWܕ-yW@\l->V~䋋u"򳫥ll\*ZM:@]Iv cid$d$F+dlq#ɓ z Qq4!XP KD jܰbc[a/_o |{tۼv(9vQen0`uP_+#GMNORXiQhC\Bt݊6u$c~WC\!ܢNQA %XTC.!P4=y4.N]+/Q:v*kײU IDATu:6avnj%on>mbvΌ<wToFRIYףMHڲ-ţl>rpu ;=pZ 7d3eho"۸"QAImR}雓p H2c_䯋72~Wp37'~]2yfpZA>rG9g&t:ə.le{Ir3O'v9k_y%g3i@ڼYvb/(ݓn⚤Nhckz1=>"yxTnH<Tt\{8RRV%>3HUl:NeM)>q?1 q 7S >g4(.c~+HuJ5%[khuÏJ']A)o+d?d^_VKY{3o6ݖ=3:@PsM? %H{GXŀb). Pg6fYvirɶqMSR>k|Vm/g9y}t,'/'pElC'Yml)|`8͆0-U&}}{7 H?^M`!aE\%pPq/\'_bQkRBsJ79>SWdx0<l`x_ح 60) J6ig&vtT|0!_35{WJWx JTiv535HR5ה2f'r3UJLdVCi,~~_*ٻYjwt80$RG%}Ӝ:/Qt[lF i!ml5[C$ܿ G9y""'o;绹#f?B#lؼkmcYm/< _9%'-ȲP'xA1߬ijk{;<gUu!Wu6) p ɖ5>^94~>RL@l.}ܵE۝:Km3Grx5 ά$$\*؞n7YrL1/lKL!wIv}ac߉4%jwLkؗmVm1%3E>oo47_`Z ɮ\ 5ǖNxEj 7w !"ٽDzR5l'/?;Qoȿ~A9N zO4phC<tk,>9n˼`s!eg_IقqCvU(10fG9*S۪vѻcX|p4Ý]5#4fgB9;jpg?7Շ )kX(Ml`Ru]KPo3?QN@JO Om%8"1# ST5h^!0A3iW1K.łROjA1sc,Z'蔋wdE= ́C0q&jrRbv}P;3D @M _;u:I2 @$g7Y6C,}sd6C+rfA sW.dbkd軒+<_eDF"zI1? ?4zR3o{0g?0㣀KJ/ֈy)CRkXIV,J *R2%eަqp^^'qjJph9Lw4I#ff%5uQ#mht2g~?nAuMVdo~b#_R2H6}嚵85~K2>rhx-qAA <Jv}e&A|C`5dδYU+rRyڂoz!q \Pm-@/s7ҪnQ :^T$,մK/? l?GL zCA;q56oЍ:_>e"Ch?=f˚ ^&Hp@Um༺Uls&n J@E] I6*-򢇿1*TՅQ {9Zm *#|Muip`‹DJIY}HzG6O|WJ?1wc-ND}9Ļ}YJ}㖭!@k(cZ/E˶$Ye>?KLVˬ`|t`<;1%-CcpJS;nDD"Tlܰ*L{$2Xu`)0$c|ܘj ¯o07a ՝45T.Rzb23PtA9l 71*|l=M#dǚI\q6k/^ψ6;)]M㖲,~})O/;GʨBKm<~qA?WӓE$r)[lʊZh0vkuCRO C\{TE;euǐO)$0t@H$&u萺pj|TGBΑǸ}gp5x/i.6`Ki['j|N]4%YƩ 5͔Tm5jkewۦ ' \ҒretPe Cm:o6Tքtԃ#)>-aS[Y-´\h6iY扷 YPJclMVPl#p^-J6OmfVIrʣ]OR|Bc l>M(6g)ڪ?eZ}]M Zдڨ,[bm!BkO|"Be0AzCr fZ4Ɣmu!}8`(_8'*!8%XNΑ T\qL zPyV-GRÖ}ĚpW j:ڟ{nGMJD&gs1,9̢w3̑>b}L;Y}0>{?k=beQ8 M!1{w>3ﮢ^$j6|ݯs zpfl;FEzY%­ᯩχA]D&{G0_[3$9OZ%2҉H3>pI}3ZvFё:fX8PPHPǢSMٻs)@轋4VtWžuݵ]D( HSLI2$<fsK2=_5' {CO iut' h"ؿ_9idVO>6uuIiRYf)wou_Ď2zII9s$m&\M8ykpBZ")5\v4,5X5'{CK:H+~ϲv>,gO|c6E> )%7 S/lxg)`3rAKzѴ tTl?βpÆ,/*sgagiNf<La6+WK]u(vlDʟˍ HOa^}@.?EOY|~ᓉi$8572!EkypJ$[D<_t }OkrN̯ᬹ{Fd {j1_vt+.8c#?zNl[~-p>#彷^[ #)}:6(nӤh?;~fQ}o6o[GÅp5eZstԵ6}ǕE}c`b\yύCuIv)w}o44{p IĬyˈY).^ Iqn_%;vUdPN!BMQ?v [[{b˴-o51Gq-"h x)-НD:jlҲ g~y=Su.|h-=R7zT$BIti }I"G5ڵ#/xݺ8S(t>Q +ynIF$#bgEqu\ %h,EYC#oK(@(\"6QN!Ku{SEQEQE91:5\E4:)((>-o$5J(((A6Kӝ EQEQEQk_I*G`EQEd[N][((i ]USqXQ XQEQNaNĺ %iIdh2D4H(3L((ZEQEQEQE=hEQdB@+=G<0V*(9+ *U(~CN`k6P((%+ *U<~X]hPS(( +(r\Vl'شOrIĨ"EQEKQEQôq5vEQԤ`EQEQ̂q EQS EQESkv ]%/;-QEQT((J[U*5CQE9O&EQEQN9RhEQԡ`EQEQNnzNvKEQ%H(0+w Vd,(rjP(('J+(+4p\((;IoI4؏g <{ND]5Ja.AFq((n*VE9 ķr.M? ":^n""4GUX_#Vʺok^Py5c@E<>y#dBQEQT(rQ#wsG^MK\"v 0yc]#N8 Sv (r'(i :S%fo+dzie .)o1cIT$ r;ٹxgI:׷[gqIp]s]~~4H3Ni$|4÷e'b,,0[41hV`: _҂ngh[`sI{BqCo6[T ,Z\F#IH6o0yEim gF:m(I?Zb]u ɚummHDiSɛ,r\Ӡs, 0a^=#Rc@w3S-ٜ`Vϧ8A+پ3-rBۭn0NM`<3^6.6r`Pon1Suѡ$ag褙_>[ .԰ tkw,_}$\%v4ZJ6n+%Vh uU,ɮ^͗kT}@5:ׇRC6n`nx@L#_]Aȣl[p.^9ߠSLYy# Wk?fּ _+`Ny4Awb&MQEQTXQ%A'.pO3)ufMIxRih cse%e.߷%s i"7e1~uPp^C5]7Z@RahA`ݣ]\oЬ0@9 ~p8LI@ؒ-n۹6FǓM JOSܬI3\icZON7)A3hP``$ZNsNrIf%i4[}8Bc[ǣ+(o`EQoyÊ IDATfNt`Y+R:;uA&K$e6Q@7)D #,EjUu`,JsѦ-pd~ JeNA'L:"%Œ#!ud`ؔЉ I=!--TmPy~F"laɘVm~oa67(swx6 RI~uA8A69S!3 4NLj?.VEQNߵk‚,wVycזxuCfd`(Ͷ]6(ǭa+&wQ)aZL5qѹk2 KLAZ@42I.Zj8aӠ2m!S3|:2ɚ&:kי/eKgk }} GJ*tAځAbA&ta`6.IG83ƕ=uBmxP[qu!@t[٩er u'8pbsN "))Z`Z1O0Aj1v6.JAbw v5hS`fV47Nck4((wo/iR*xĴr_L;73VP=Jғ˪j'96D|y/46YןNq`{ ?]z|wkBzX`>W ۭװ}M5 ~\KQAvWYr6̙B gzouMq{ZSqN{^6X#\a1}3NM<[ٱ,M?b^i_wF))5_r;yx/C6|9^s9x7|&?1Yk\q'48u_}L/L#c#dK\qmS>^H S|$ vӦz*rPeekL g[BQt|C>%ᗒJORKEQOH)O'RG2~K" R?( %N]&QN6EDpnl,EӤe:,wmc~f=ʘDEekLΕ};x|}g{/co-J>SGoxWk`ˊ7oi?(q9~G'TѪOOY\IZfE[Ʀ}?G-pQ1h;M{[n`w],t#&^/ ޗ% e -mG^x+\L6wykӎڵ݌wBQ_[ĢC:qVJ6 ހrY5f$[!) f}C[Rz_2BiV /w̴85]+¹"Q*d~jav3g FH 9dv!r0ki\÷A3.~I7 方ML+;sc|903)4B ^rIxW3k>WvF]+#%w_Y4KVA(rr&d/ܣ ~M >ɒ}&ۋ,.J7Q|=s#|'kqk\2pBg,x~D؋F:wBLŮSz|C&@2 jӓ)pެ?M:zU[㒙xogBH{NfdmEIւ׋1+hL>_>dތc܉r,xݒZ{~Kr2˦I H Jk_4 n>m\ZOÌCm'5o-˺+aU)P9TH`EE['Dd^܆g^ޕ? DH([=g?GA.K^~xnF:V5X=m,/ 'caziM7'KoYٜRs&K6UO*+;j|2ojЂIP\1uN=ZY+y9;~XIkZV9[g5߼ٿa@*x~"^hV̙ΔklZ앥4֜D̢L3>VHud"+SexŶ%`6/oLHTϳ_;b8@jk%l=1geq6$:l~ϏOg,g' h<WAOy"Ztaԛ3io}hp@ǚ^-yR~nڕNF!J?_\c_!8 bOlXMq˸Sl\:w?ɿl"?)GagoyXZ{~-6o~Rgk絗fkpMm1p7J BF>9]rU? SY5gZ +>bl?,s4פۙ:ſzysS T䧬]un[ڸ49ӂҮRVEQN ul{~ao!3G]Q3Y!!i( a?xqq(]_נÀ^4-OkÂj]2v5YoDaض>4 :)֯s56P– c#8c\$Ko52ss ȫ\xq>'.= DxЁd/Q]?Iiaib!{rqW/󦯢 ܀!c? 0w}c} ߋr^nS͛w!tq$i˖y;cng E*mZQ?J J63",Rķȕg#0 [3*Ly&/n_aZ,X^n#rI5Q_EQP%Ѕ^V6SYx,o0XRK`_%4goģ?{ddڂk}Hy\|vHJ!qkB .$7(F{?woߘ_YWވ nA/ghX{ƗrS^Bc n0]{FO>7ku|&w:ZF2u^nޚ+nIDYtj(ocӥQ0oju1كwwᗸGwrFPopW8ge@"|œ?չ|$2g#Wұ||r6nZޮtwX.h3jU\?9Oml)wmPvIWڞFZ= %Z=ׁWGNc^^_&xY^ ^; գ_WWW"3É?rZp-#PDROãpwg6k8:~]w|c|0x?'/4IVo2d6-'hNk#vyYEQE=i5>|Fe{=ǣ?_)0W , ˢOw9߀2?<4^M" m>nPVh9a~ 2~yvz^$_v_y9m0^ ~EQSVH nՋR۽*WzEh߰*qJA` j)AcE$?8\]10s݋kg1 (g#s'W뵚ԏ'\ʭ/eIRˁDiX <}Np9i@w2#Iʳ>ŅoU.๏5"hڸNAu<1XӪZ[p8seJS-=%d_0`o6ٹ& )Ǿ\v@Q?i~j+EQ4 ŒwaI IIPW +Zѱ ;vr!RPKenXQEQOikkS߆EQU[%" vW pyc4t1`# \E IDATeɬAu_8 W'Uj`L kRhU((ʟӂq EQQFo]*o#^93O`X>OLC@ ޏ],QVU6hiP*}R >HFo?|riFSsꊴЈd)uKB4Mg/<2Dw.q֚\4urq,/bӺm$w9l%o݇TƱ:s<7O I .(ǚun L=e/@Rm*Ȩ'>)-#Ktfnnqв̅?e?FIFӡeVO'2#I& axW.K:uHS=(JZUԸo]=UEcੋ[els:=;>/lw FćUg5c:3ޔ[in>R uycT6"]FuܽqQJ^֊KxIjp;Bƥ,Z!7:2sisPkLr8"\8*wjrv 0t04x*(Nb"Zs-AA-kէ2 \'o$^97ǵ"CgKņ6ٛFxL-M~/k>vΟk3S9> IJMCXWEQNs?$h VE91 Xnw.wCFGn>wߛ:;{yY2J4>]q$ )\9@yޞψ&Ù\zV&jU4A_a2gNCG< ;֧ c=-g1ۄg7yW4^io<5)quAj>nPw_6w9L6m\`_E};⍟ 7)X9V]CƯq9!OSF*(+2VY`EQ9C)wLfݿT7/0215h92@ D :W xU-4 !N8Z'R9xm!gqv;ZqnðxͰihgʝ, .D&b󔐝Sè(",:USBE#ߖ zg^u3|x>\+zf^ir{ܿe N޾lF&=RZմ g7v!poNx?^Bi~fl0t 8Fg3K{כ#1k,j#%;@惹$ {F.l [V펦Khg@'=={*ozvƵgBY{sȷ>͙%HF8(-g'qPV#+~¬ L#IQhX@ ٽ7wJsZi{ٸׇ{6X8͙Jn )T#B+ F\{F# +<3u+,+EyI⍌5l{1$>,g`I>zl޹ߏ[O|}RN"P:^nPLJȃMйwQUy?{wz/* *kˮ>n}^W^W $Lf{I "(K2s˹I{8 IK(^}7ɬc]NF_/x$G&-+kL2"PQTh!E0b?{'c-*MΣU-T'_0gMs!nG&90i!n ;k#vJYq8Ɔ;; R1r\dڻn`c% {ux54"8%^|=r 'C{r+ut7\ÙE: v6ur\CeXޡ@(C˩&>xQOaDV(toffɾoIZzQέwF}bd?yYw|٠x}./_g7􍧹W)yd;ʦ_W^VpQ]hzq3k.-o*x&TNI:-hȔIņ#-\L vO<+-[RCo{$~IvS5@8[S+6t緷Ԭoju4w?(+SһZCqwaL]\}0T@KH`޲ݩ/]]` k36ʓXTŨxE\);ۦw2ql![cCH&s F)C؀NV3[KCca l2E^K̍Slf=6k9L8yrKqVI 7BUm [.zk-4nMbTN06Y ٝEXU`%MC Ķ+o3UvR55b=00Tz{iK4&<(OFFq)ZiҚ87"}{klټ((B%A Q~RuօoćQ 9qY;բAԀq2P~: %_ Y}6x9ϐ~< 7ռz ~A0W:v7&W_>},='sȗ_;ok-w1²Tt;%\x zo}Y \d7n59oFz *?o 8]h daWY?$+{4 #q-WΕ9g{gxb[\Gï~5gϹӗu'*2~yz%װlMi *vy={o|% _H߲/vqL:{DE R~%ςPt>YxCXNw$s(\X]Omo2R|]I-[?S6q 9kysBs0 x2i|1;gKX\9VReV,FiC#BSUJt'X0sZ)ftV.@$]#w?xF+K v|% +\sY #ek=ީԴv(:Yg'ci3` ZX\7"$g6C35;7["%C.+H㡕 (pPI,^VM{{B~#-T3oςM~ lmiJEs۰|a 9$TVx<\Qa~Q-J]Uזzl`Hv⭅.d$ZGV= Vŕc$uk<^٩ f[|ilxfN7UGNp1+&hEϨ[$ ܒp(*G7=k`3H]ͺ/Q4 AIoYA.zl:h`!F=~M]9{ha'!_"uVG]\l ?HNfeeJ5Wz빌j-G{Z:~V}^Ap}mhnH(I `k7uTDZ'Bԗi{ַݷN>HD[h,u}$6=I4JTI#qgj@kmsuH8h"3 :Bd;Ҝ$<bߧ^t*j{6-BsRJ ޶]&q_#)d{DK QBdg'ڞňzSU "- |'Df8@Ƿ۟;- K}˞K/swI(bVVM4I~TVV{ ?Ǯ{DHs. qY]f)6f٣qn$l[x8xiOC@0d$ۜS \ R*4Nws 6/$8q\4~BL pR#˗Y|LͰM/& Ao^`JPqC ^lY,?-j->듂 m6$۵`9!In~K3yŤ16i=Fyzf ?ylu,4^sV/d %y zmM󥚼A6Q]k*+ɴ$h֖L`,sa' =zj^xe/hf+B>/Rl..HuL]ַ}J'{K67W/o R(ηkp$\4u:G;>6?آiφVQ6c.?Cy]$oijm&5y,$نBI^.y8pDe_a|L0ٛ5HX{~Ҳ?Q& 340Ϳ}u#3 ;EPvc`-9,NwJoYeJ쉴8]c;~q$9=rdfgvLLﴝ['dd{Ηf@:ddu5WPowgv4S Y]/\e¡XarT$C]pA? ^y (+]^){n^{5.n63ط+C&8|I.h_jb˻ lCQ RK vk˂xj Ndӣ2ɭO XwbSSfH?),la-I1km%מu٬@ך`ZK y=[^d\;ZqTS1OLvyTk`CjɃ&[[vLi}o Ino%u@(vd\3Nq~2Y*p_Q7x &TNR~ %'\Ld6m4wpao*R~`7,4Cz*f`7n}{g1(cĶ{ȫry(61 0hsq Ƒ8*c% G+Ŝ#y̮`=$[]6u<+* ,d"!Uc{ ?IǖTuq+ ^,ض0P4++dI *WH2Zm:=xIAi) U:i4;y&&})fUBMTX<9eE5m5zܽo_YEscb+{[IrŠRI$[oXsojZ>u-z{6O&ÐL`:rs]]wx31ŊVTy5(f1{:d Sުk*-XŐm-, 7ZYU6U]94h½$9ҧG(b(ɚԽ{fW*nbP,l9~Gva4 qͮ`L$SٔZQ$mNh{4 @C,]sL`VXrD_ᱨP&/_EjɦcD[D!S|(̵jdj.3kbЉ!lARG֓Qbs^url-SY}M(ٻLUFԱ;<;@IBO< 6kzdiN  mexQ2s͠k`x_IPRf?yPPJ' Z;홳N)i_ ~2SafZ%C%~'=6J|M`jmn:l]!I6H YJEf0tĠMm'oRsT`Y˳5M|A6}v4Z!K2}M`=Kgƪ}DŔhh-qݻ(0 _6 7<_; ,w&X ~ ?\ *jqf uH4q~|0JI{You8|.>?5. owM V$X0/MP9o c qζkۧaY0c⋰ ҥYDHf^d_7 yi5IXܶ.;' .F}( 94t:6 @X&IMCbLڂ04Q?}fH[5C '}R=K l>v5w߶A+uK~2H7&u&+M}+[+ B$?,Ƚi~sOWcmo:m'$8tD5^۞4`D'io52osT!pBqPXeC$=Xit}Vj^3!mAviNasH}1MVӶil|ق0$ZJݫNܪ㽕+ 2usf.?ojgq 0 8#,) YSyKiOS!;gnRSwo #rv1XGyH훣zu*]{I4Dh'55nKf-OPK.W}^jkܯlmqMM|וi<=h&3sqMk]ֽ;9D=m 㕧i:A#ij;FҜ7JqKbaaa|L ~{φaaa!'5Qa|&6 0 ]b^aa|Ji6Ǻa|T;,ίY0 Ș0 S2} 0%@všЧ@ӧ_0EhoU0>sebaa_ f$0 0 0 6 cJvnؾHs3-fdfeE=٫%*}D盳^ a@ƿ`0l3L0 s 6 @Ǣ-[kWaj$J=Zq 6Æ3x3zF0L*0qL|KYp>֮Eh)]K+(}qP 2 'Nb豇uKl} We,Udq|3C 9odD*)Zkl!GJ4֓/$!֬bK&*6m`1ɻ ڒJ**͹?x0 }&4 -1یn^T/`< \d 8<;w0w,_RJS )%P(!DZ܌DvoM 8 L"x+K)OgIc}a.s9.9ow 6 8^|Rq*j\VFYb;#cx~-v(T~"۲a" BV |4 _Du|e6mXeu~,4RkP>dd4ƊߟX,$;wңGtPK?ZH49_a H<a'>9ʏjn*. %.]\͐>: Yv [HY>@ˍh(@DvQ 8F&|eV.[g|'(|+7k#<@(Dkk+D@ H bVl&7?cӽ`fsEd8lNTaG=x*Wڨ[ 7MABGoq@#}tdF[$V02^r4k1r_y?xWhBp۾ ?kMSS XxZ>B 6l@YY)$(2x S&M\v20v3k;Yba|ɣu KǪq~."VkRȄ+{4|2s(P>ZJ*vFf`'2m 6 yI&捩aYj9+-ƲNo_MXMt$df-A5J2Ö&d0~,yZZZ@[CR!X/^adE3 8֎J%ټe  HNF?z ,'.W~[@VMZN6jID= #'!¬' ݌ߴ 9EY#<ݛjBJ"Tp9yY8B Њ|!8L Pң;M$#mRɴ\7ɢmi/0ϕ M 6 0#NwZoYqљլXn .]8k(VԚXK: Xz###dקPKX~= 7_WÕ<ҋrɲZ Giga-%Y#+fdMZ8{[0k 0#tk-'~ƌ+FݮflC8TVng 3``oP-nAJҰl9#GK$$h<1ēa$vqOxǹ;1̷j:s+yYl⏱q:q7Y~?ssgmU;ؼiCwOokLUN\[ou]YGG -V-:&jS-h<¢}֚Vr+%eG%;qy :z˧ _;ߦuA* GwY ogQ77C[5%A3 8}nv#3;kw-5<8op WigbJe@W/;,ŞU&9Y$Q:#4 +齇ZU(#!b׊N!wĕeY}3j7}b}&p3gp%K_/Aᯯ|Fy_sט_'D"&;GR͆q쮮Nu\AcY. qpZh4eY4G#/)999ĒH)Qz\7Ɏێz\:&/Ou P0wb>˝YnBXL.[SyJH.gۇ?73[_3\ك;w lkTb ?N~9LT Nc(gzC6rW:k晼xۀ>߾o?81QA XݽkڇC+B LJmw"%==MMMa'c q\Oc6D!Qz݋Xa>Z nsǶOUeC-Qjےaɩm&Șbj׶h@zŅcl3nNj}6#fvy+ڲl.Xe~s#F:W`5+^_yL2uMIˣڷ;aT$PXjs 4]aF]il&ucm J \*Қ =Zh͊.mLC FZg 5@86C4<اÐn65;=o+HN09֍ 4U=^ض],h#zȧ2 =m#v-fA} p~/wj)$vz<1&L+f[ ]to/C)lN.$j}^\豦g{?K mn牅>M@^E,z4;<ԧm@IA?l)m5=\h: Kn=ca'v9\xמ?>Om{dj ?a=[+q]I\vh++O{"fc9ƱaBXxI),|_SM0Z()QRkt%[7]E0Xmj먉d0dp!MV ,6?ჼ%P=C2?No3o*zNw//bPGla9o@`.ޗsXԭ.æOt洡ӹVK-N)J,/6|XXL H! k('k vmY$8 ?%m,8K!~0Jgi-th3/w`{n0=[SY=,@MQl~zSr5*tж!JEވȦW۳|g~b/)O) G6zw P^9mNҹQ$]20 - ˀw IDAT> M%~"7u[mICkR㹩sDFqA̡_Bd&wX^0a߾8X 8wša+ڍa8kH:i~H˟Ǹ^HBBa7I3ztn&na*zMM x  'f= ۭbh ",/@+|3=JZjiw V?XʈBp<,~Gud 9[okqrۏuhk|H'1,ܪ]uqB%dui[OQr먍U޹ʢGQX]=-RA 2;< aNf/@H850H, :gaAeCgȋelȠ`۷nx2kh$V̴i%h ^'m`G"w폊nyv hL pb8Eڒ\8[绳_ >-3 ~)UYw'w=j}|Ĉ7rSS9+ .*X0Cю% n_OUzJ2tc6q%9wqg{)Lr@0aCYe;jEڍfjrM$I/ce.k<pkJt+vE{'>/S$T%dEtQᆪH,\xZ N`q ]'{yNi~A=2JlNٯs繥0t㱍mr)HJ7q՚N.Q3`ä˝OT2nq0y gW]1ȡoT:lo'Yl?m\7q7bYgyfo 'i2 0`ENY6PV Ǐ/)әQZD L͒KRK@L+Bo?>_Ϡ$;L~ʹ=yb݋O!eX2O6ZZC1qu҉2n@oB|_?x?4 &yc\]w|d2Dvms^Mks$AvNNiqA=hڣѡ#mYf +7^p)]ܓH3ms3x\64ƍI}K)-, /`kK!T=tWѮ-;POvKM|ɐ Ѭx[XYqIEP4ٲ$E ţ;V@s" k ׷  Kn+.=uZl&U>]jGVE9yԱn.25Lu.]ͭA4nކN.dW#:6F$ dE@< ̞fLEhucLUy ffK:7h UN{?.XR9dr_cž=7^ KxVpƊ N210Ɇ8m-n7,jdTމt]G`и IIC|8jGIScRxp&̚4o> ֽ閼KĢaVl݈3Jb_,!TС8 X#40D~G L"p9\nnI{IIQ.AtG~0BBw l"]^[7T{a{,ĸѹZ/d)j\b"/IoȞ2ʹm{.`>nG=˂0m{w)=ADXQ7І(ߜ36mia4lFfVֻIvl@7ݩy|R',Ô<ԯq8VҦa*BtԯJ $`7~1?H7nVLQ"<6 Ȏ,#cJiwWsd2qdn2U/sYA.ϹiÝ@m(Do]3l_1G~>^'Zܻ j4V秳cf{7k&J #ʞf; ࡸpX.p+^]`hKv{x-L A z%;?[ |@:2&tlDtAS&zo@8aD%Q {3 ˄].".$~y7; g_fͮN۲ybչm~ bc^j"'[lzo!\6Ff CvAeI%cm M1AF}sdwX ΂3o>F:Q:{hÑ$u2\1g(~p[䟂Sl1:aVto!kn!%v.G]6U}('Qd#>wy)yu|` 4g io^VϏ,WnJDфdC Ƞn D';S4ɨ<ε^&uv40 `j(#@R s#]bw^;oa9_f9s7tL={Ϝf #s^3g_Ȥ۞`SM7;s12~ [, 7WϽQ._ofw zz(<*f&κ!}[uY^kWQ,yul ǃ#n׏4t /;SWH|VӰa%z#vah !_AQA/xaYЕœTOvP$Y]mruzë%¬`U5jM tpkL+(SqpxҽfsDl-ջ8nA͡ w$;:$:=9 fJt*|=;H$Ln@!#Gg\?B%_~ҋ .T4c+52zU-! avv_Pc𡖟 V9tqMUjرCvNm6txsCK../qTsWGρWᕍ0zȞzN/5 uh$"Kqy&mu~/؟L_FyP[Е\emA.n:t 2u4Ψ$% m^) *{΍A&J7 u&@'v8|$<۠(!Npw9Ͼ"c t'BLIJ*ޒH߃L FyA,Oa )aB471dx ;w ۗ2X/ ){4^_ٱ׆t^3exI xq1u,0 ?8xN. O0DIRIli:zߧK 4w3i p*2tuv0d\}Xm٠S:60|v}^(Ǣ_0Smc+:!$K%$]mR=Lnn mn7o*9=innɚ-j:Vq/d6ms_a܄)͵??&Z7w4!sxZ&yFBR(7&{{W v,q[qS/ZC`j dCLwYn?BC%I /x8s;`Y^Mps6{r;.0pn]mvl_b޼&?$58aIm^ۣ19yէ{cX@t<|% X3t0ovx8/5*_cz%ln}bO_,%%)hKr$ _R.$/=u1~;)u2H:<4`n9JHNǙ!82_is[NO`9e&qMtwNK&I7,} V *\|g~4U7_>uUiK|n~ s=| ,~9OQ3hip^oS6A ދ{.7I­{$ɦ>ò&q3!ao. &KktnE?Z:*Ǻ9`-ymn_l:| Ub|[ѐ䶧,Z_beK:3/VEgKk5JGlx&G8pnp (r2u]ضCkb4'R"mdjǠ]8[RMc/ѰI2$.㼻:pY-fӣU̞؍;{^ &EQ7ndS%tbdʹ4ua:{hܵX"%)/-.//S[:K)m]Y5b88Ehf`bnA :ò%j ktLt#}0\LD"n #g˞@~5H K$R=䡺$n[MަfOr3&,K M@{Xbi[^\nAЕZ;yp"%pc ]nʙ컔!RAx~!U<}u[ի:$-%OD;g4h\vE# k vLq: Kbg qs+v Y>EnrA8,z07O!3]Kc^P`EQ@KmC@4<t? kMn6p!4Fn~揖#B0x".]$8Bqlq ~<؟đ`C\Q8yWXr)tb RQq5+אtKt~[Eiy9QOWlqJDys t!eTvP P#]r7ҁwh%]闖K'~p g'%]VBԹ?Xt ӕ ~!2-it,ɞ.Ikgݒ_H=MD\%'T4wD%CG/o9zΛv'S-HfţpeQg/XA^.t${>aޟKZO 0DWQ$:0l!4'D5Y:B@.FOx\qf2>݄c2} hII&|&V~LҰ qk_ҝH^4&`pͨ[;ʫR ŁAibP;l {ViGXۼʫ6iٹH!+0w%)%B z.U7~EQNc8uj:iTePAKSu;$1fOc&0<~ᅿ]hH33ut+~4M,_;Cf EQ>WJ*)+B'[w6M8mr8J' %3ga&ᄅ4{_#ՃT=SzEY-{Ǧ8uGHƨXQ:xte>7Ι'/Ip4i4-}wq|~ ' TJ!uÍ ~}Lw;+[6mJXem[+k׬QUU~,u(zpLW*k}<&).w׳aD P˃lkli,u7۴fC/3k$CeEVӹ6ێƤάy6EQw+ن߱زz;8:" Z1hh.`wGx,[K8XCŲ  {tNUuR 88dǎF\vdfzQs!zhAضNzNlLggfZQ3=X(̝K 3!F|b&VJNQ>EvZdbPE9ٌ}CV( b14iNkk7flCG&r44IQCb`.+H ~}D0/r|Trs楣q gR3d$MeٵD"8b6wA[EQFO<z2tkHE<.3E2AXM|7G+HcĘqFՐ1TB56nn :qhS ǑlHAGP=z2iDࢬ񓦑 qp(Ɉb(_$#%dNvIEQ8`{hc&d.w ,!cPiӲH8hN[[0qAsk ][9u0Œ2ٰC.;{-dxpM?B&ݴ򡘁`*(_XYٹe0Ճt[GI@spÎF{{dG^6~t 3I2Gt MvǑؖ?d=a.K)]"8d0+sE݂XWp&_*wTdlR&laRg 1%xS喑jhtvG[p>d5#3')mNNNK IDAT4lo`y_7#/#>Ueǵ|l^A+?%*ᕢ();x侧ihh$ Q퀁C{{3a&ս7;O|  ", 6܉4%.)*+@7`ٲeXLj֢:[` 4QWx5ϛ JxmbKĢ1…c n{븺…m| \\{+!l}n^hN~+F8&1<DQN"h"Kˮ-̞aEwtij_ԑ(ʩ똳F2 Iտ>"e };lL4vƳgy=//̸^upɽe>`z~)^G%WW_…pv=O{H%,rL^r׍JGwyGv7֘Xmܓ?5ti#ԭYOjұikk#;;ǑhZ(W8~I|<*~ p ]K&IsnJ׾eX>Y=ۮ'<D3Btdo1$`왗)i  mo]xG'4*_lEeP3u&§U#ȁ/((r}өMdi3hކK8z7эpE ;g<[v:]$fSQ3ݧ=w]H0Фغ~#egeּq"M6xuE&vE(ˇmulعx/m!"Ms :H=7JOA)I[`s)#F1$#pQ:n"-Ɗ-LƔ<7 _EQEQEQ>uHJOKտ98@N ˈ1"$}zǖݒL MM3 }^/NO o((zWA"LG<ɖ/EO+$a}5|7Mx&u_1_ND< L5\ ՝H/ A+@fsH;JGgc-(((?'JOc}F,@8BZ:dBD'Xrtkdzz4 NH6[|b{L/S̯oi @ gc<{3WA+ {d7Q4 8i6M{L'3XlPA((( )H >lvӵm%Fa^a6Bӈj p@.DZ)GvUs0~_`D+B^ n jSL,£'iEvC}Ly5âE,MJ?Ѕ9l_Sf2m,KpEؿ-ɰWEQ9$iCIln}K>ة@XhB vltH$!a 2n C&{}g =R.}I~ߋcCWǣc҇[UnЋ66<nz+n!#ر /bO?|SI}LLp5E;61'ċbj(`oTehz{3~ l'y#sr n$27N7plv}7@[K#-Rc :v>dpy7h_2#<=eɅ?)YK^7j{`ٹ|@ N^}F_z9Lƥ<{s>o็_C+,Ao;[fwKr:zvFaLW۶)DV&Dq?鐴lM>Mv’Wp,4l=%8˘rh$B`'O7AcqlÃ< e^w( "%&mYk`9]8n`WFyXS|>>/vv倮k,o,(ÍcChԱxiōwT 7=% {ѓzM|5I<@1Cm Pƥ6sn>T4-~_*Le)h=5W_1Y?{:7hD#>4r疓X,,K&|R=TVn7\tY 9( _RN~I9'عu ͍iޱƝD#]X $ '3 (0oa 'M߂/t\1 LoCtknzHnbeWT&؛A^'7i!KՃ L`ϊߥOlf_]IVƕ7add?a {,n8L>މ2V[/|,7塶Zng6W_)+Y71b#a]ÜTI.qJ帛㌜/+Br+a3&}oD&ӯ_c㈫Fm}+L{D'#Hݴtۘ酩D(&ֶ:nN+BRC(p AƆ0٤QA E9n2E:Dװڨ0y(je -wK(ʉ$cqkpg;μ1i"(8nMpZNfd`WL ,de\|3m[ZK*X$u?&.-5㈫g-cb'Ic5 C,%>6dP2#op͇kh-, aZ[K•p %! Kv8U;9\؎WQEQ# (.('_N }2()'' %l2 'bv"{$Pda=`[hYhEmճ^܉0]I蛭ˉvܔ{Ou{&n&w~#C3'hEi8tv}C( %#2~{dp ag+o.ixX9REQxS=({MW]蚏!儶]Oѧ_GK%.\?eJ/s3px>)סVIgz%\>&=j0mr {1~؅) URܴ ]/<,R4ybQd<>˟ 4ӪB@m-ZCvpFg}B"fXȬƍgU dz.c3I6*UnEQEuEQE?Ƕqz&#&}xCY\}pGDlσҶ1>.m] #ۘpe884Npwp2#jcz eK4C7,ID |~oW`t&_51tM{ rHDt[:^o HHr:6I/'_QEQOM*, ^`:_PŧV((RCEQ#e nj.R(|`EQN $"t&tÉEQEQ>?T(rD4t'NQEQTl((((_*VEQEQEQT(((|!XQEQEQEBP(((򅠲@+"4 ꚡEc8tNvEQE9HsC#)ˆlIyTUm EQN|T/XV/X %OvEQE9uz;v<&..)yNr!?R5(khh88>,x{}*UEQE4FHfH&VpT(g^X1oUEQX X(X(9CVdԤ`EQT((W, t4.%)V}XQ3ET((ʉ`+WMD}eEQ>ma\(|6VnK:k 쒜ZT(LSÞEQEL-$L|PE9>,^(||Y݇`EQ+ooPٞEQE99bIx{}jN`EQzz((ɳAT(rBWQEQS'IQ( Y/6f_i.>tL~t3CV\"`3v_4\if^⬌Ϛqхn3B|g)4pR4ŝT -[wĥf$7=K'^y5jro4Εn,:KJF~[l]ÕyHҥ1407@es<`^AMƑTUsqE&Cz"1ݯ1(RnFpGc!Ж礞1{Pҏ̫wU.&|߄W0( 1AqylL )>nsT8FA~ tw}ӨI?KyDbԽq pIVc`5_:~^ܾg$gQ{'<-r7奕+;-c^٣()'w,Ż0n6ӊ>iѺ}^,9%?Ck@x f]ٴhx.u1Meb̬ ]gmkxndd:̏_;;+rV}e뼺)SO A^%"{M>j_nO$<&G('\]˧`J6HGꔚ6r&IuwM*Zr\J~1{('"qLl*γc?[ǹ͟ρ)μu{ƟH9 <典D|ޏsrΞơa.n$|whałIZ뤾u`W%3wo[|6:Xв6V;'\PN΅>`ő.L՟y+NsJ|ˏXh Qwp%ʽIgLnՇy;s7;1yN]u|" 8,p>p76?7`{%ŻL̻vn~r;ҁ 6/z}?ߜyx[i=Sjϡ`ߡ7 |jt.*tL(Gk만F4OزQ2BҰYmtXs _H8,Xb@иp4oy#&;ј:`jLHlxmn1}A$.6`jHnX} npN?xū;{Q# ή%KW&y< CN>z̽ƬIeQ'?uΩ)HVoyΡWki?`[rtZdk ؝o-X**&MOÙ޳X;չh@cW9;[fpH.:9U-S\vg[ϕNG54&9>MpaMGS4 EL λSkB wv:}w• s2=IhIq98;BaI0,^ .(1խ!#dr4o;rT%I0St330b|aƪ A–}o=5n]$/I =z;%ټ}'yWEx Nmʷ/`M˳s|;x:r啜* Z(߽_22Aa3x 0Z60=yDN 8 ͒6Hx}2=$׭"#3֐=-4yߘKnS]xw,\yh :G&pܼm {}s*HaqrQS}ku) 1LR\ 6693[$R*#Icjo{`[@ E%(Z. \|NM`d̨'Qq`&Tvoy{\bMDz @bJ&O"\^;^9l6k/`Qq} `Kg\?_C]B/ rUcRo]<ܷڶG٩?9L*ZKV%& pŠwĦr/j&[E*xx`~-yR>~vYDʟ'/K,2`IV6Oo2cUXkLe`*kf:,Ms0%I֛^>+6Wazύ3dY /qsMc.ZnABb.չ0![|i-_fJsA9>ir~v?r.hğ15M;^f0!]<8@ >Y)iM'kmU;I֭5n1e]eg,Nc^DB^nX/g-pYhdZo m LdٶBCm%ci0EqЪ}VKdSqe?Oיz߶Jd`/yˌ|@Gɭoa47ADbI^:񱟝ĘbRO?W-)hsXk[Zvҙ׽*\'f/s+&RÝ\כNu1A"q+ gq mxcev"U&ZgS#Fsی@1Xq_eɨ.ts4G>V{kƺx*p*E˼̞ܳsKtFj4z|x.AR-<3UcB;킁kqLS:S ;Tñˍsߟ&8tLۣ"BP_77q|[2ku&*W̟ xni[|lmjkOޛ`iYl3&Hy ܹ,Ahv<(bu胯%=b&J45|a)ѱ4SD-Zt*%wKib,.䬚ǂø]GIәw'RTIg.x_+[^z԰ۑ)h}60tFOwll8ѧ[Mt;TYYzpi) n Ik;71|| FGWs}6ׄ>!Bg #e?yɎ*NIv}MdCN*VJ,$fV=u ԢEHXlIZl.td^ݓܤnrM% D ~ MV&=t LD@Y#fV q%&\T(tk&ٱڲ_b}dzW$$3@PRicy@ 3 Lиr`WG;LQIfLs'p*(tlqYȑLQC.QPd[Ol}U75Ba ۪&[Ku:6̦RMA,4p*@(ؒl>PcUH,p"L. ͚1Il϶<]nRN%.=. +,V쨄VW M `E/8ijd~ }#-k&ŖrAx83J+T.CGJ` SAtA)(qk#i٣FHÁPcxfG6Qi&+H, OQ[nJbk^O$Ee*4%n'BɼL;&i+ 6T+*[ՙj$,W7OQXv!I `ؙ:CI "VoQESWQӟI ZA/3مNx|1M{+%cQ 'Ǜ<-• 9md*:BS+A^9{EYoΑQÂA>mH2ZUEBXĨ\*S+ލ9I[qn ܽzzU!2|m׬`s3>Lv%׎F,VՄNȕH.Ƃ >҅]4D}705%\̤^$*aĹtbF2|>?+T/3=`Qm6o./餆}Ό,2eWf6Y9PXTDۄ&1OmMmЮ :$sdš*TzIV?MR =$K٫ C("/ $*2ڒAWBZ,-)SIʼn|M(m,K4IswmрgwJ8oG-&[PIm#طގNVOR„F,JC#. *=:/ }NP KR\]uM*0edfx)t9A#gFWSm0m*4 懤 Z`H04[RJ6ָtο9LUu.7B哔י (&_R`AI1Ihh*^l %VPa'1W2畀awIȮ//hɂ*5R!J$VFX֔8AeEqO?8|˜Eʒdn~{:y,۽X`$:VE*eu`-wFV=.cgoPrBG-u4H ?V:$f$ƁȪf$_U*=i]vi&J; d@K4߳a֮;L^tj0d9Pg3) p7>O֗.ǐҶ >asҧDaeӯ_aҕ *xYte>6d>o.4[56,VfJ[JZ l2 J+$;l˖+Ro[zcPW%AY,+1*C"\R(ׅr!hQd0nq@f*( D5Q8=w=eAerBO 3&GmtA1)2AnckP[ΡM>!uD括}K}l~+׸"c_w- R Vmtlf_`-cOǨPЛvb=Rs |J*MH9qm|&WĹ3ZD_LA :w;G.kG,f z#[/?#/a%TT`k7yN6oɤ ҢYӓ@B{Ed7ٵeȲ>(Pc3}s7oM]lyJov TblJ@:ͽ.Iaeo^ @\BOwj50dl!IL_3!eaɓ3 ONB@3} OrrnA󷙔W}JK8'Y2!$+{y6nssq9 *s< 3fh?^[XpLD6SΪϻ Xr9TLINn5~;o{Xj_$kv7:ޜXOVx"' Y`5L&8xV~natmo IDAT[ XD8U}бw5|@V# zφIL!0 *d2?&:x&)(0bhTwgp[fF܇h,C1Y F_^ZRaF/}49[sgy6/mFۮ)nUu'(~׭/H5 \|>Bb)SU%:::2]2@$su;Zܗrx' R6D(B^N4tC;f>$ BL|:xG9q`Y|դZ.$xyyts`-H֭7([u#A&>%*O pHoy_D {"1%زLlfl_A5Y^̟8e1oyA1UCz@Hn7/U O -XqI$Y9vWp΀+ϜK|F|KrfI/$?DՎ5q-kʍw,P8'TTs.Au(;UAC@T B@iex (? 7D"),X*%Fw*fS(Pl%-IeUݎC e咸N^kqk~9XI膤b/lj!)Ti]?}Y'N9_U0Ws E` [Q5.j84MPlqTUPmui\4Ԁ ;O飦R{s[J *BxV0 ȕPZa[ yLb&hUDUs6\14 Wd*vlzqݬ8GG`fhbPg~U!T.zPp1uץ*8\&׎=@VJ~^˼ 8P8MIAehżjTVԉb|uoزݒ 1}j]CPM"@$Ec eydW'7&j΋,|z< '6!)_ hVJʂu^D=|U^ӗqd4x%̸Tf# @y3zZ4#)XÊX;ہhNbt[~X~vc:=7qy>>Ghǟ'6y]Ț;,{.e#=3ݜ>Wk#}ݹt8$2hpkRҧJ6\ԣBac&>] uj`avGiMa+otʔ_Kùؓ >#rx*%y7mLhiuF!୰$k!mHYJeJ(5}wd~ie)͹ $81%`ٛcХD6J%Ba@/Or{FC|$IC)ϭ~ vfezopH(/ [Wq U5E__3UPc@Ҕ4aP~%yߪe5k߀\X2ߪ%cX>"9-Y 5J IG,@Pg~U[\[.UǨns.Ĝ U_cY׏6Bͼd1g[iլ*C!MIQʯp Z|N׷̧[ǔ}lK}U;ߪz Kme>dN njH|{>֔0.6Gp`TloObLTIEA8G2d8{ jtnF"&!^}K?IdykHTtUARAOϕ-erêx?_EΧZ.z^v3Wp'/쮉Ǔ\9??jB%YG}Tr@ktzo#E}ӥV Jj4C\ϗt ?gqǂEq5lo}3w?@x)oޙ7sayy6ko9mY&ѳfꤟ: Zck%G<$N!?2>WFaDk<|FJ;aVjC׆Sy (.1Pz54Rﻏ͓'3pf^~9-'Nݡ'L X;l{-^ZΓ H jPiaJEJ3P.H&J7iXBlQPjnBCJShhJ.tT,Q5 45tibAJʲ2J* WM]uޕXl*'1M CZPX6MAEH>TiJr#c!'WY_;FU᤹@B<em, \F0# b'6QZmddS -Veh c/}-Ve%}ugw~-[NMeI:ȸ^Hdžq~?9&vQ)IB QNGpBc#͎QW#z`B ͵hཪ뎼pGnfrXuM;ơR]|wV3oᒟy нk'ƩbK/MEaƯrmYo[,|*(2Y*$5~`݈ XʜcǂeϙY^NeaG)^ /0 yߜl'JYBq):#0#0*Bի0X٫( d:*MEA&ZthY?HIӑ#O{saʵYv#0#08~Zh)Y?r$"o[.[ZT{0k~ysŚ>�#0#c lb #0~ؙŊ5NJ]R @X&`p5ؙuR訋 m.P'W.,y'js.qrA/MIaFV#,2񞔺._( q~=/{:jAW"SqYhh6-ά RZ"~D*tٴ_-٧q ɍ "hBJuI#07 #0]'T^ hB D$t!pP'xSr\[A%.8ޛ%-54bj5;W#!lH Fn S49O#7g10#8 Fa*dfPy!3@ ,}rh9.( {i>]WƔ47%]j l.(3bJpũL8 b MQOOLrtz)RJ *4iழX`QDAێuRhIX,dSQ d9ŪAROo3AAݱIURTɶm4Q*cB ֕u+PKS7EUVH!U㏧)+,]kalP{`i*2(KNWQ~)ƥkYtHg:C=C4k3a_fY9S3 :K >̬!r*aΔ k(5B~boVYQoa1sd AM%_S&پ]T=4DZ[|!8s䫌FRW3Xſhθd*( ̙3֘: KҸJ[r0&ϛә t7|3Ƿ,Ec|/$Mc;-*iaFaFYPy>Ň/?}5;6hTMR!y1kS,^%|H.]*wMwqcЌn4dL$Q/ %&s\>$J!T'1()(C[5$1~f𳨙~٦ZƯL C=]g|;th }Bw[X&٪|c&APϛ+LrV}moT&6X˦:mr|AYDLw0߷ FvxK"Mw`nm ohTJ6]##yu/sK 6.NR1* -&۪=;K^xX+ )6RTE,TFi+[7pO@Z+lo2OHT}h=njOJUOP["#o|/1Iv%2U!]vu$ٺsߕ\Xlٞyi?qKA 2O?d| &e*P5#GT ABq 7$rItkrJMUɺ&W䍖~g^Ҧ ' 8J 4SdzV~INoST蕪!NR-ЃŶҚ)L!EH>̩9OɎ+m0B e=S* i [`p }oФJ IQ+B@[YK4v:lAPG>4tU8-BWnuf@AMUN%[i\Q u$!6) =.@[*Dίi"waGMXTi*$d硚(5SXUo<InM4Yp:ozSIV-?P -0wOWcA/ +o'K0dPꭵLL4aᩨįrg*Z'M@SKغv4QWAQ9ێZeGY[W"4LPeb) Zpix(iNZUo+gE.M KASZUV.]j$rItcNìYl7U4?;>~A!$$0HC :Xkjuj몸' DD! +@3;ysdTZ>w;<{iq>p7BkOXo5"a8v6 # sP?-XPK 8g1*yTeZjmȧlhe/ٟS_66H 3/K^{m*h,{'rE+xQȯH~;ARF6qtKoC;|^笭 «ϰ0.6~%gf:Op0蒫mtqeRfK>Ɛ)Iw]vxlBèIe/ a?.V.(3SMZ#jzI?;~%UoH6{o0#gp6A=a1Ή>!IrJɀ.I2汽p+vݹ0 (np?j;č^= a8t\$Iי3%@}00֩%mâCOJ9[w6+_Z٢au-m7 P^|'jڅ\μ)8R IDATz*-hS'`Kt ~6I wl̤,u^\~|:X4%:|1ٻ_8Va__`_G.԰]0V)ްpe0wH3@9P3.B46:#$~+T+P;kemidr;~$&*kDJ.~B|9D\jt\@{Z:UD͞ pXDm _ƪ-dһ zUocUijgǮ-{xɇx,,Y\r0u^l.[p~kƐiͿVB.?+N)kh}я׎nog~<~ayKֱJl*jNGѯ$ioXM~Uyx6Nt}<~6/]zfqoϫ_ÐM|_Od<<9[ۺM_O<j^%+3@<7s/n%ӸdZ;qѭ_ǵ= Tllq2xɇϭq-Y:z=޵XSpo}Ŭ 2h@Z*o'=>V|JGn2Yx}ۙQ{[".O>ijW}7elzj[qӨ/{%yoQW ؙQ(Y&}Ձv^uT `5u 'HIA rrFOPhUY݉z&%:d5$!:2A  O,=~xttD$]cEF %KPYL਎s 6[(t1,!(c$JtʽBǫelT_iD 21#E"@tW!р_cMNL#煇a`J9 FYO_Vґ@%2 >e*{2%vpue*LVľ}^-jyYB)0+vtUh h,<p(m>F~:0mLnܓ K%.gdHX(h"/I125^*R)r(LOK d.!O^Fd)jevTњVdQ LkdXx󹥤(tov$=&g M)VtK205!)Ԛ(Ʉf2"ֈغ۹rBzfLPՍF!5t&[53c1K)NU4FR $F{ T@CnmWƏN&\ҕ1#zcqu2:{)>'m.vViU%:~ fb;)pq`hJnx ú6 aj%ĤHSٱKɀD &3b7)vSc۷~5ãCۺgrq Vw eTK黌KyB{1b=zhx]5ryQa ޢ,{*:~S2S.HSFO抂wyCEsĶ{)W|&3LQo]62D=ۯEƗ}|[S۞$?RټA;$^GdWil禇{=u=Ԟ=Ni^UY% z' t ?6N$EtJm'[.?+Ȳ!f]x-K7xn+L#xw@ '7{C AٳЭg.u0Hʃ<84 f~o*ϟ&+%P% 1CMGWy76t\*r5&j_ 2aJ:k,h7${-ox6: |]ljzUTy1_֭:3CW5WZ RPc{5? 2,W牉VMAuQE5&ʭw2w?xZ+ )P_UioC64>O&݈ꉃ, 5*c${54d̓,|2NΆ ;0uh{=:Ke~oBU%>^٧7mtl8Gok>7фtAR&`\m*soB3l/J HV^ #cXa]0ƕ2: yggD@kz'*ÎoI:-^¬^U4a73saHbm5zV橧Vcӂb0 ƥno~ v"mrp ${"3;n$|hܛJ<򱯅}+yq:k"$ 3 +ќ`VKofd䉃"{xn|fu5"{g1v?Sק ;. I=Xт GROnڮLH+d>ܹQ=p&)ɉX,f^߉WnAÀDE 7 `o]tL&~ EdIINlyR X=>Tc;f7|(mWZCQu3y!aTt6}嚕6)zhٚ6B'*C4\eF0 h~Z_ӭn޲K:qW&T%^ WY>JPQ%rg6P Jv2MO^CAR]hVrF]c ?Cae&wZU],;|u5v#UymNEu޾ǵ$"lt;Ha㚝Q\*oI" ~NU†N49BUIy66e]~Ay[,ӫZgji!{4ޟa9t].MVywn ,лɹ5TmL\А(s_ϊ lD,LKzW \7}&$/9 KZ?4kg)I w֟x$4@&@j֛t/!NIjyrNFڽ߿XU[Ek],|lIWu|d6ۦNeGu'_F>NӐtqհ \׎ ~;M9ckz; )oq=> xu]zMnk! |m}-v̓& Ne2ǩITr.AyGe t۫&?*#A'9FT}umˡ0jDlt蟘螜?ήMBU93]mZ*Pe#VFM̎ۓ~2V/ 5do.gF #m`Wm 5?9%[G8, Q_W2Lچ& 2n~M7RU@-AHoĶ: ШޝF0(>* !~Ȧ*7;uu0x$ qtsztOgpXM,[ʭ467,'ǮYdiSi/欢dZ}>96~V[̆]G h躎=̦m0v!DUZX'**'O:R?oV,LVoXXt*v,gַTߡÇ p2d$IAsx' 9[#|S;*HrfY/` &2c]iW!++%grb+'۝$Kұt߭zpUCyj#Hth}63t +/>lـ(sߝ]i!a\{i>3΄hF r 1zr'֨t݅f 5H\=ΟsxG1uT 8 doGA&4:*wKYCRڶ˝=֥&"84:7|aX9?ʳ=VQ7D\3nILbx}q_-D ks_eOY,fzy k\7_ߟHB#v\)Q3*Ss 3EOp|<2($>\aoo&$ǰwF*d"t5@&EA hh GM,6/yS V0G^j]~$ g/x,D8MHj FLzZAxKXl6\hzJÄJ݋QD84P b;;0t@PER 5먁 t |l-ƭb۱~MhRe:j533+ -%U `D^w] LV5kA||?B]4 Vs,7&&{w]%Sӵk\RۋG-/$c;;Xx:X.pgRLgrʶ]\0/:;|M Go v|ڂZ&ϭ-oIfӉQo[>w ( =Dsބs5_JMmKWQw\s'tZ KgrI4{8S v`ߋ#hn3*g3hOQ13M[~[џ[fL#%c۵\b:G* k`w_ !=AI09s`-N9m\lI Kg"mAIP0ъJ]Ȣ+X6K.5_JLTWE=ϖYSc pX>5?uiN"'CȢ%+N{d xu*n$HAe$^AI0@Mm7nS6eNj;! 7C)裾M5^p&_XB-**J8N 9J g],،*"c4 ETI!ՌQj_Ǐn`5^j' a1?\zAMjqlσG3bQJ#.WAD"*^hi\ Nj_`7ɴ='SH<vb1?(ܰ®Õ} Gy~Y#*<~3F\H. <gBb*,*7O&э*fLc|T;%4tcuȰ?׳>UwY a,m}ѸJ|t:zADD,p W^M"nk3~yd\^y{#M-m(ƥkn\xϸ8 l=o}BB΍㶌\U!.%D EH'm S5'9-nKrsG"U[_Y/dE`~(IVwS_"^:59cyD~|O 6dѧ(UOS둈}`C&L*G~~C`Q[C T"cyWmHիX __~ _͚ˇØtm*[7]T"Úӝ%k8R=*N ˄nAmxN0G84p-5.T햭);Xb.8 FMl\CGg>y\EI=H  tD-p. =A BH]ìfQ]*W끭ik(!Iv ObvVjxJwSK$ƑG9w7 dJ]A 8lPs% I!RDBFBK^m,׋N`!ir UR$:B-\5%(c"e@vV.b!'i)vj`BB0P}. d`P{M˰Vuz_/&'b$4~IQZTiV/gsyx,2ed< :)HubӁ f2tڝA'ZA~ vU Sn*Asw.??)USH1X27K;aWr-ZpDQn\A 'VXqV-5Mv*]AՆ0؈j^7nFMn؁U@W^1kX_wnbĬ*M?jf$Afs7vـм'_`22-;HLJdL)]M@7%2,um/㒈z;SA6D,p0ڬ]sF8ۉ\|h?]:kwl{2=@;H׺uQjQ0, Lq)Z rC$ ƭBZ= IDAT͈j8BhyO+|fB1PЄ*[X%}~~-cp:ltcF vٱS><̍GQI=uԉh?JNMH0:jr&$o^5e9R̈́DVOAHebϖ TlB7IvzGVeK1u:ؕ[DUa$2Բau@GհH CyN.{=:.o.ЛT*e5lP;eL`kJ܉dK>=*~% AO#`L+OydQbg_pm';]D'׳eқ ~/%774!^O.$kq\pXYsy А_K9|}v5E# G1j9ba}<{\.r\O3K U3}Uq ix&_2(.ևp PYDw@oJ~-iä16r W-9F$'^N㣟3i,MV AAh&.Ac q]UV"0IWP0 xg*W\.A`u0Un7̀f|LN>(cٚA&apFvsz\n rM_fЦm8MjPEi1ps-VksGWQ0X'uQqSN OnÄ?mi$ CSZfvbݪU0ӯ ~]iǚJ@}AA8!,p֑1ۜKJVVR |&2FDGsJ ݀b:hS%!9[gZ7-cu8i;l' YM'`l(K1AO@R ?ėyX &ӉK ObJ۠Odrh{AANH   -OB@A:/5gG  HA' k3  КHAAA "AAA "AAA gp˱$f<]9G]*bSt^Qi7 Sݔ`"ypQ30w_RԺ*AAvF`]7n7pYYYݶqFx饗شi[9[bȄ,0kdohժuퟙWGɝO)tY T̤ƨɩѐc2ut96}A$ g_&Jjx&tl90HH"Ok z;{Ռt^qMxhh=r+0'cJztTHyVȭ~5lK=n,a8vRAchzW)  Y0 LE?N$I 0:?$poxY9eS +_ }XC~lqq[VtbV'o״?%1صd6p9tISIW0, J~FIξΫfFHig5`א~dy{1Jj 0JEs4L5E{"3햛138Ƀd 073!t >:>k-@M3-Q!.4voa&عBH1AA)&#&9**8IBF@?/A)9E{} 8r-?<; G w%# 7|]pu5_j6&zI3ٷj*n*#E7#V=@?TPʈE&@q]17 +dS7>,^?θ?}UF y[{2Uʾ7k68B+/5%;ٲm%;ް~{}GJ*dŇOqݓ+8Wz Gƴu;[>5ɠ2#>^:Iuqp #7/}{?M;5o7%U.`KVVrn\ض-hu53ck^#rPm-h:[ܸ<7D ŐS۩/#{ZJrsl6VuIogl{P ̀S8  ,aq,"&͂jFn$1">E.;W.bKX*\.g_kx?_яL K͠P{PWuH0ٖ^?TH1iH0A*}k 5wΖE˿s]wϮ0:^6:T1&_ n\5 vo~6I([3]L;ƿ'w㔇?ggw@dSg4"dwxȁL :r?G_kd)oM튌߮ `aa"‚ر3޿[ྞsR2;w%mS27{=Cl1":G'ǵvb}aǀ::3$#*;8knͦ8(ΰmf t܏S9  &"cM"YvIln1cP~PU*,_" 31uu[ޣ3fu&W3ה>p S?F&[@/'' {7%q 3E!Xn s "fױ$S1K.gѦzzٳW"/ +Xt=ŝSI2B.vV,{7gY0 ^.0gttb%2~lj(Q?EnYCN=xbɾǒDm:`+[-_^SG}$O`bZqV+sUhTob;1^7a͆?D {'n[qq/HtUK~mٛ&!D8mEU.6AXWAՌtfdna`{r$g,qͶD5^{?{'Gծ%<},zODehfBneoLۺ3gmN=!}WUpùmvPW=_>m_ZI 4G>ΞMm ҥݖc"K$H-Sx5.Ce6͎j70θqI?>t$d#|`0v%b?z`";ixaW;n;9u?.oaW&"jsQC9/- R,/@4h/AA8d9Ԣc4NAN`jqy} 3̜?7Q-n?~Od0a>զ3k2>6Sn^/kˊ)Y㏧0.IF[*gvQd NĂR _=9L֠|f-\:c>,-0K6'.Ǭ&! 5b1'yv)2ClX rqu||}%ɔ _ƻ b[oh#Ln/6`2@zw'ч)6l`-*u,3Yƨ3<,_P@iW, 'j6_AɈ<ޡ3:EǵHkH4d МPz HaԜ_gǡjb'οeqw3߹P>L]UMvCF E-13Heց;C?,b],߭p M2LEmچ 5y9tKhke{kx7__f>Fҿ_iP>|8M};Ok}DhJZ=*{..¤0z{vj%}x:>_M`!!}Ko={>CKl]KvD[n4"Oon74o7ހL\@@I`N09;>oCWPƥo]ʪ& NШڵ{ "' F3>QpX#n*ݧAXiKtEWLjKcGE_}Ȓ jt e7NGSd$ft=n[ЫW21Pq;n܏^FadO+_mÖ:g0:7 l&A'pǤylh?+1 2ڲ_淝9Z50 WNB `}JedKn&_ T-> YOz]G8 3Vc4PTKO2HP['Z5{t^ XNI9OZ0rRftɋ<>~/LjURa:}QKSzdT@q 1 %Iڇy+A([ =oF*n֙p%s߸ڴ?g?0X  ,IM=UGg{UQ|$)A~<9X,YS!+E!zRwI yA$9wXF"ⱿuxQ0FLgE&joI60*&L-,#K 7=DަU|*r+ }gUލ\{gH (.XG:ꢎjZGcO[>YW[qƅ {CH#! ;5h(\.kOu4k͝oǹsF֨WQ[ELl͝3vYkYgܮijl_R~0qJ{1ұŽu#l B Z3}??2эʴ5>3x~}rM>cc9Q:6#Ab2c$V5h#  )x7VB#jY{MB >%V4DSK -ŇKt,X$XM\!qlGv:/v$Rvoq4yq@h躖8icz%:k]7DHHKI %["M4L[kݽMbz Juz?tkc۹lVUۼy3ݯ(WJk]P&+ўΛ]Jg17)rnMc@+(Jo ]Rok^g in-.xz|+ SO%+$/"(B 5薢"q -0uAAome,r qףca`=Օh9aY_(K3{A Qu+( AM{R^ا& +> iܲ$`6 DJ% $A8u"ԉHMM(OV[4G$WLvq,еokYPOo"Z[{!d=t b|#Fz*U4EQ Y|f=6EQ'-^gKoOIJ s[v`!>/lEQEQ'`/_ޑѤIv vBpK=>@jB0I/`ȒhB'`|LBJl Sc)h~2F:'H␝K-0c!tw!NT+h~MiCqJ4WeM(óەb,>gaV"N8F wVN}7X;t^Xوgqsf*6h<$.@s5$n@Y0@~AR[fd!Z#$J Fk+67b{>,_=HS? ]14z9l]1C9gLz/",ziSEQ!i$k#V9鳔9~~v$'O>Ԥd `H$BuF9Y,YClj0 M'uxeD#vSZ?)#)Pg~6ônɚ#nzd7UŘcr6o2{`^~k\Mۮ1 *oX;}^r/ax=/{Ϧz<2o9y]I VcIa9 =MQEQ/mM]Bti܌-,ą 4M P$L0؂E] l$)ci]#5=4hptd -u~KE>?+$iw?s| L9kd\S,y|HIle0 L> >dNKG}H/tqU8a_Pp+n5U㐜m^YQGd$W:XR`h; =VvPiI4@mXXv:6x"s^x;;~#0t.^˖b6:[o[^p3:͕%mk`E F%nΓ^oFx5^TJǤ{$B5kLĺFĶt5qbf*`R9ha?lbv&f hbU#GNJkYA9H<>HDz#%^Kh=ЄLX)|(,8]CqZ"6Q?%]>xm((!7ޒHoaC;4Ãƌʼnn7v۲rHL# 6TR\< ǖ4hyu.5y:Y6nQBJq5pf/!HvC<&tj81IKlZ@ޞ5H ̨dW3 tIٚ;AW MIȤK#tAQIl7'IIsv6܂d=4hJLoU\9?ߝxkQKW\vIAps9ca-DM\ .@'k+<2KCh:HN鹜[ _~'ur,r[~Co/:6S]!ߺ5rSo5h84Z?47 ,f-^KI8K0JF|{/>g8p1&/:⺃[/@|#ej&3XK<4S3Sv?B"&˩u產ZksԒ֠6קxXnh4iԮoy'yT8CȚϻO/!x܎vIg|*(J_h u}ۭ%w-aL!hϗDtp 4)VDhI`x7Sxr1t>ٕ3uHϧǬZɺ1xf<4oETw08+Mwٷ$'dц؅XXDu fBd !(9W[f;̒OkK~x>96^voݏ׶}k?^y{fWrÄv:+qj2F!oL\ Z1~.e+s8tH*v|y ^GrK:ٸE`3KX\Q)@d_R|FA868ZM7/jfMkodB ilY9ᗿ;Ɍ1!qfX捕C%5?.`oz :m#D礪W1~`U1n_iOExNI" ᱍ tXw@Kt=Ye^s:<?ɲL!L(0o &K4V2mZaxiqu-f[ſ_6YZ+\ԑy 6?Ɏw'@FFLvÛL|b)H@ c qK189(CxT˟4Yb'~+U@wMu3uK'kYܱe1>O6Hra> ˧1$U!-(lE.bqF(>ͯ;(ÐQ<#Ƹz#$@wxrkLԱark~ >g^}g՚"ws %̌qk_犩nn8Dz{a_蓖B1c}QdV^͌3rKY`qt4L-B/,SPi\\>A' F- U\/-*b`;Uo1mC#0BvFA?YeSsP24ZnwX(3EMk,EA֍64Adq8ljn;HUZ|U6K!;Yqx|p&lSh:z ;,a{=}Z*->I7fY3d${pAEC4U [\c%֟ xLbv9%:.zU V|!{ُZo2Bf̘e}aĈp)~B\߱HٴKYuB>{ >rh 6%`0m OgYF m~H ز:JK8X_ĿVMw'v!&Ë:ٛiYB&޴NIIGxOWiiR0a|q!衃9&-KF$XZ}:ϣ\fiC6GK{xz]9ZQٷS6%[9Drmn kx͝$e}hX̗ ^@ie=#E8-K7s%*%)#Ns 񶎀`2;_.,ZZLt!5ų˗((MxC\K%<ߵM$dkK&a~pxB-Mlc㩭?okW2fѼLVֵtq} -ۢ " ~ J E؍/$;EzQחϼkv-dбhG<|;$iX zH7s.扣`ݲ8w}jSHI]/P$Tpǂuܐ -DkRIϞFdg$oWhhwMUdG5E N bf2í?D0ml [dlk1L`ٱl LtzHA0% B!I-HunϚ@ۗ>JϏ.9ǵ7{Yk[H^Ѳ+#0Fxb 6LJrn^2C!fV` Cl.?n?OJ)5Y3ׇAklqmJJgVyh&p^qHi ơc QI7nzeQ4겊&x w׳ci@R8q䳹n{> o.?Ɍ)L<$eվQ& Mr eȩ~Pv.P;`?R/=m۳t|~xK8m 8j0,EQeI 6ttVI\HaAZRp$j:$hk1;Zꇌsb&ˎH`'1O_` |6% AڧIJh`{0 O+SfQ0ݐ=zw=+rx(oqn-4E$;L~Mx- =Vc+Q$N:9/& ~s?\F"^ 8ƨd8WkdP8-Iy gt/Gx:qΏ=ɒ+_-B^%mr_W~F;K%Ok$1ɦ%1_(: H VucwhHmP p;{&m?%5+!s4e?+/;cXpu{4xcycKMlla0M76B/x3Jn2h;;dpdffqh`GA /bVe $m C=6@y)ѿ2jlf!V^JM˛=[ffy|a}0ǔ sSW"kD>٭ -Pֱ%Ψq:L!5ÊϖX0^Z(c+y˭mD6ne kyV2M̨уhY<+#8tL*kR\belH͑}>W8 ]G"VbH{֥B#>`Fy7KYs+zl}jVH 7 Gj `G d z9y@c\D3['pC^M|o3&2 5u/BJܝ-Ơ}kmfW'avurľi=F&Jrƹ81;q>&藫r4 @$OuJ̐×a\˵.ƴ^<|Y73%dAR/l)ٓsr~MIkMFt/1! z^S}Jǻ8:-r4zz !9Yc[,* o7<?QWj-i0<Sj4ltؼpa}~1oQ;C"ŰCF5o)̩ڝ{3E]8!aE/dI@ɏ1kyt1s8e1t[#L2F/}G.FPy\R9\uIc"ޟ1(HgSXϧ2TXQECE9]К;<%@\:x8K׮g3|G XBGhKYQop^vFиR-Y^n~NhcSl8f+9(Ӷ1Spǹ>Ns$P6_%2e&+|2fa:Cox;eˑ|fkaOG!YdT7^"lCfn{6st?~}cQ qnp}\$i8`1Ƌ}ysCOcgx`X@4Zo'M` 1Lp}\1vm'{yDp{6ʺ]D ?L4ؒ7߲ dImC٩1Lp~M2/L]ڦ`7OE_au4 L_ipT%" f^՗y'b|ة=lxg8'S1*zos<__Mq~閄,Xfӽ2ZqAf[;Ni2;1HKaGW''2kLnb?!ľOCܴc>vVB%pB&qi<'ҽ$:mMwu Xe x氍麮m1-ML ܺH\c-4F5y9&j!$wGp)m|P~ $,mԻ6w0-c$:>@7\-j2D<i#{o,B-!I Ipv%HͱYNn7=^I)T1w 3nAhArCXmeIr IӗL@'Ѭ=%#3o޼J/(WJKj>%6+͗Iw*BH`u猣BSp\ż9/?=4]w-pqm۶qfW![@s|BOיڇRkm4Vw'F]G $ =l Rȷv\1;O1#x $GKԱ\IIMɎh"1$ĝD C03Ļۛؽh ”407/ HobN KY~pbXDkHǜC<8yuH 2QBwc Н; pe2<0$MsASHbj'͈}Vt}A#Iأ׊Jz't^h I"m Z\6S?;YɎ^uM Kq].ߝw(:9=뾓좝S(w`;ވٚG\g#w۳>)1q8$-̤lZپzIsh mğ7)[_ƒ۲VGcҤ}y9^}:7h7i=(>O!+䂓[(L,zVvm8@>!x%mUTB@RW 7@4Rw4Mơ*&+*((;$ф;DeEQ$vYt"))c"08ib >?qˤtWr:Z4HVC;X#MJ:w`)hA4%ѯYN,ݤvЫ2{Hn(4kC,#FR{c%eP2?i!5)İMI?<çZS8KȶIryk ޜcq4Ƽ :_LgܷtIct)?͟A((ʷI.:3g #(}^IG27pp||^RRR4H$_tۡ:ɢ|6 LQݙ2IyO}VEQEQUɂ #̫%ۣj#͜x)ȘDضEfv?LğJsKz'bV~dzUա:S'd'~SAQEQEV׸bTU`˶zq7EV+$c+@]KC'CKy<#@()# N2($QEVQEQd%/3yM8Q&1y\_;MGV`EQEQ倐,x7ۼ2E(&4د^D((C5/_f1Z)|.X3@gbߦ:ZQoj(u@a}m!ICHUA0|nH~E9E5sj((|C(ODQEQEQE^P(((`EQEQEQ{A((( EQEQEQ+((( j$EQEQEQWR/5T(((=[u9Pc+((||`/q *VEQEQE^o# V(((w̞Nn)2ֈIG﷏kй+V@+((icbD7)?n曔ҕ]7߾խ;$ci|_='ŶdK=5FrM #9!ܣתY@Ӊ-h5s4_K|Ÿn~m=7_ *l^qz2G80"\d;gy+E61sLQEQE98[yLswo\_lȊ.kXAY7`Ũ-/SE0?1}{[}S;Rm}PR9 oր=RՑ^>..ެ+mzTɊ-} aO;l 4?zǓ7ڳK :ާ*gfb_|¢ơI5!jBm8΍/Ƹ(6Y]^y)((}0g_:קOX [GǦ{*4_\?8s:STdҼ}}%c ",}.>׌p֔s7sv~f_ ?<'M{I>~}a=;{0ۗތgH [k>cb1&u꛰E`a[o sxmӳMlkok,xMc?j(} mWCQE^7ޝ[@FtYɩl??IJ qCy<Y\藺x )mw<<\5&S\KۘGly y6WHI]?m2UzS*5k3{Ψ|x6 :ms3}z҇1*ךqrU-gїeRA6擏 "R:ɛySΞ d/s‡X9B t6-_a,bwys _2TDYڣ<2a(`zK_EU0tM>Ãn%}t]m;g3O:/cF#6×yy9F'/群6niB֦pHI.:UwbuS< f[ÖxeӨE&⩧2sv|CK!]+\B>{ f,ن?87-}{N{?7`ɐ44E5CȖ yjOo1+Y2 I# f$w?:-%=?:&ۖ}((]6ϘՔ˄aIs'L6i^L!#}tud3eO_i/kiJƈ?= oD<']&;$G"cFr% ?Y3BC9bu 6>*,}[-WtGxzZ}Eؑl?\v&kձ"?cxIKM!L!nuuiuy=<omIPş.*l*R&)- 7CIuXxGQ-ݞSi&/|QA"dG0xqQ]1TwX t*ͽ+܌7ԍor7l`pa^zwJKe3?.;%8*VUI?qN/:^{{ /K9CȬh%y1i)/p/b6v. ¥O1?ryQΟ9i,08?S.Ϛ i.R̫U˸)yW?*ȺuA|'@Zj i)KS7lsx8U35JVyGUejf&=t{ REAbò뮫kuum?׮(*"@zc2d2M<͐ .=*3?ȄgQ#fe ⚫:9^}lmo w}0_ /Oc<&p.Cq[X=:z%.0\t֌>Kb&F^$?S/ÊW!Ĭ4·ICrXUAvRC yXf+LޜbƊ#k h0ua '[> UgWƓ[WޠqsU8i/^=n[MSSV!;Kٲ`)ڸ9+6}HZ͌O%ZDNeMu|_⺍ Ðhէ'm#KCt+A gxe,ݲeje2;n<2.藉A2e,ܭg˽0i2G_IIH =|]ŲIG/,ʢ+Yc?RuZAݧ5}l95/77 [9wLXYɰ95pvat^2>:Q?ظoTS!tOeV?O%ܗkO֍yA6+~$Iݩ==맋Cf ˠlqxk<}KTEl)QVSI^ӊ]od8/VdqQr!"fH$>ߡD9Z7ȹ[k,'%wWƟޕC?K#0 ̼?d>u+r7Zt:01xBŤc=@ 4Q!On}2|1 (PA ɑsֹ |'^Ωe@v-?#mS ďX2ʺY:%L4o6g΀L2nۍuhiǐOqVlY鄰O;qvIg1u3uy{w=ڞ˟|SM >C ƫ%΅2(^.zdiCGsztn%ZZ@26%!e  E>'.k$$q;W%)J[E|` X]NWT%ᲄ&KR L#~ɂ{\YsjnVW6*/!EŢ5sTqi5MhU2z2" ]vm۶8N***(((`ͨ0|*RsUM†3Fa OhGUFBmnT'C3 $0;G0Mr]sAx[^5yu%bcq[9ɲ<;o@ 4#WkęE9gOn:AlG~x v#3DV-dMvbkя+ ߲'Wyk4ݕGc湸 {4-&HjaZɎ8[ %TMiE+[ sݻ*qnIJ܏-dJ+)ja'pJr6c-~$sv)~{2R3MQC`Rc7eItEb]x=M41h aJ^ʃתj63MR[)ؾU1xF tBa ) 69]tY:Xj'nz]| 7"SmUOi|;3{~ê|s?;~g?^gŔ'y 2D@ v4wܱm@VR|.N'vIʏww4}Zn>-SRm7t/^ Y|suR]\TbbPw t*vzuL@rcAQ'7OwB{2_2.bUE{1x^I+N[wRF]]|3=*13 ׇ0{Q #&۾/y37#,y3h[-0b\?R5Ƀ(drL Je4j~ :]]O笎*`Q};{50 #HrsqYQ_?2MBE x Axc|jr:KhzoI l8fMNLmo▶8/;rJxmnZBڨ/:UDD}$miJrC|D;" K@ [<nj'6) wpedZL}w:7gO#`Ν0; L\IWS{u7"}<}js1OYg^1{3x#_j80Ǩr}|``_⡡ۮƦeUXb|O8!V }qSl6նP1o[_ǐBʒc%h~$t}?.#$ jAܖs.A_hu]|z:g3|psm Wn Yq>-k=v4sQu}~su$4v;  ] Y[5wώ{qo![Lf:nߡcf4B>r/)u:Lx*I t&ny_{R*V1w^d1"5j={O-"}8i)QQ荥# `%5ă }9{30Zq]j=ڢ?ے܍K!~.8vh.jᚑ#[QF߶|ۻHD}m.S&pn/;;?grۋOn6mp/\<ҁm,qm(;+'24MGW.}5W,㍩TAgҵg>v:~i 5O?LOkms)1T-残gWF ϸxM偊ߵdNޅ̘6WKWELg0:`9lص6`A(((9<@TE!hڟ$Z/#XɁRݤ61 VPWII`4բ a k IljOa#Hd+5+ `OI'z(.[FY, -H(zzr4{ . hAB5jޒ.RԝIWAIE%)$K&zHÐU,5114 MRhCu¬KR:/>-ԂP#3e+IJKmz&a{S=HEE9! )bG+TŁR+4e55<8O5=7J̤T2QZ@Z13 y8PiJMUoL.2Ӝ&ջVfb/tROw<4(e]?nf?c+)\jG!Ab3C-rd[p ܿYQ@1MSב2`? A4n%:6r\Eנno!Y2vS۵C;+hsM}G~&LnakW/d|6T}ej,`-]#WnO:G_C#5$R#%IoW>9$W?b#/Ͻ7U͵=:*X+S_߽^J|>f8//ڤVG̵7Nj@ tc~II(Eluϊ auѲfMݧXmbeKy}+_E w{ر+6R3jGqfŵꂄ!Q٧$%f<jw>R䕹V?>$0ke&ۓi1bN)Ŋ;%wueI"#6Y1eIug ng -_@ƑU?)b'%#jeX3{$ȒJRZVMwe8z,6O:"r#N¨qJ\g}-B;WdMAwrKGtLp"Kqd_H7 !*-),;~b= LGD6-d^a#f/ .ztϤljNc@dW=h ]Ȩ?85!b!~U7[sLcRɻ'g}:GøxnWw[f-nzG}R;R^Eq(\vjڹZr8z;N@ ~UCZ࣎܎~X⸠p֓>u6mr}ܚP so0lItoצ2M{ `QڜV!ZWs@}X;TRCIL@,^ۆDp!X߰q0`i/י<؂\Ϣoh(./!u^⦋c 7j8)+(IF@Y 5.aʛ"Iez>ު"9z V@rVZ̓'YY.E4u2DH (+@suC4Ozr§@ ~=BD<=FsEpn'R:x+wR*'{ z{\͟Ckk7B&ljr߉klȵ967z{qF0¤9 &"٩fg3)ǨdECqHd&{ҖKɬ& 5; =剱8Xly9a&)LjI<2מ{|-s~&%JJ.?\O.cX; 0wD'nK~ذ)@uWIFxI $XybiF@ 0L@0 uh}GqXZ1d\c=p3TrHdm;dQvm" ḎnӱxFaVD2qИhZthBވVaF۸ rwMԻ ǖ ENgl. n{;Im",�zKSxad='uuh=``; Wҹ;6`U **-&Bmqnfَ u^/ A  I C8l@KPy,kmⵍ9,56-r=*:Xx,knfWm">Ʀ.l?[Wb{gOeܭyChzkXOj))VrV] Nk{Q6gx+EgiyOJx0gyc{Λ<(ц~(![_/x,|kC0g =x󱷘 _c?2H@ jGD7"t^G~݇5b iG%sd>`~<7D-ۊ`a&(2ri?=Ƴy!ӚFom^ѐUn5<~]pA|P.~F~g')xucy{C HȊgJ+Wպ#n͇%n{䯌|D6tH?ŸGernfΟ+Iډn_~9Kޣ Yz2>zwU,u?{3u@K0L| ,aQE<@KS+~F cJRkE:Yz˺Xc BxA #`n4(,omvn2L.-%޹Qch,)dr/r`[EeP"ƙV]qݓI[AIeStpٔ:TёQ z Uxqqĩc+/BMkHJu2C1en2@ $%v*94 4 PŲ@QX ~]ffm挥 NCr,r}&7mK-W(%IVpLZ ZztZzLru_$YU! 0WgiMl?kc2S(RaMQpT;i *Wr`M.2!4|d4jS7c$+Qɳ$d%Y$Y$gJZm@ _'nP`&vE$8m(USoKtqwɬةi¦}2 IK}P]* :gkѻMm]Q:;ZFÁ?$[m6 H t%G'P@ G04"J$  9z_co܎R ~v$b00Vp}761kƸ@7dګBǟW[^mE@ @pPiۣiּ'~77BQ7\,a;d^c emoY /y 6QW] @ $]̺  ֮fMl+Չ!^ǙPTkZl;Zk`Ôᆭ֢H,ۦShu)ܦ6uCdX)h?䳹{+JM!Y|Լ>XCkFº( )I؎Gcy=~GSCUX=8Unem'1yV(f杔kˢTppʰ@ ?ao]˾O:К]RQ^̙Q(J|Ŧǯ)+C28,µڅ9R8ڭ95>}DЇ(G=T]pLFP1ڹ>0tb0 ϦSjbY41{1;ڶЪؽAi:Xy㣢\6bӜoؚ{: )" ު8FOQc˼tg=)>?xwlgKg&II@ ueW+aɌϘ {m mbN ]p:?ykȻ6X76㠥;Ąޕ`HR ] jA p,pC~ZO[t;%:dɜdKif;yw{Uܑ J86jAbeSb,p&!/6< CRP$PPem-ëv5% 4SB%ʐW%, ݉3`Cě+G̲o9ʠB-r!uݑΰs3=,Y@KuNdb vgu5u^!K AgXl+ba90 > X @p4QJ =N:- {(w`-mdfp;saіT8nM"']o@XVgyboLpn̤*|$j/MIUrA:멗ӧ1eI1lb&u䢫c\kku4ȆhntH=:TWo{뫐 W ((w5sx7~ཱུd1oY~Ģh7Lh,Z{b_a`i=F|Rv 4:Jn\O0}y0Z6fCc#hFʒrg&Oۂ2n:[/J q9lew2Io׹@3t~\{ I%$= 5 ]]OݗxiE9! n-rڮˢ2 ^zYx8ƛ=^Ej/`tl,&kWא1C!>tDl?AkwՖ,rsnĶjkj@z^|S~, aNyL7pWtyYHRY=o#eI4hBΨKlQ_ udi$f8#{ʀk`^]D51+AfnL1 +FLI:X I~l:ۈժ(ʤkIas2ظӆ_EW’;ݳ|,71b7 G,sÊ>ʮb MI-XtY l斿]OoWM_H?]tS~yN;ߋlٟ׳9)UgQt>kN=$ɜ}^.d}x ؎}I(*eJ7p=<-dTJp.C=*"VqZ KZwغb ō\@ C'QejMF`uy#DӑfiKx>tg S/.ygVٲr3fj҃8+nWJJX%!W'7|, ?W uۂS&!**CX=f*nCo@ _ubjYij #;YChB+][XBCxm +f1FD.ɒ Z4P=>/lP~iQO{jVNp븠uLKs?nTVk'@2N×,j<>56L~{W5Ǽ!~f)Yt%)IPS ˉꫠ<m݀,NqkTT ֐@7PQu&* lթ!;pȀq3pt%~;dL?yK iw-#Ov}I{GhIDsIدk83XpK+C֩,\4x@ `nC͸ke)oV%`^ocJG8ADB;][koTVɯLNpf%ectjY2kQlj7Jޙ]8y 4߹ϰޙm˘*vLDeOB.)^U&`Pq%c;4$N*`  cۈt-S.dCi01M;[`_*Acނ|,]{V$n ^t#sWԉF/d],\;ө*Rn _Gt6RZv&M/cێ2&ػ3m M&ޟ_Ԋa=jf+6dG%!P_m}݇Nډ!^|U-1=.:ca!g'[0{g0|=3sY{u (߲w^Gw糘;h\--N@ &O'FLhӄԞ'36m*z~*GlGmohhD{f_eY۵eWE*TIILs}x]QAױ=7g}K&Pt/PT N{Z0l6\V=J%u:VinR*I<,qMoLI.(mJ$Nŵo? TdMÖ=[. f )/>v+ΐ*p[ bk{K&H[tkS-N{n)x읧f3ApoK85H:;g"e{E2ND (nع̒\o존kwA^;Ip1In}A]-rAhU:Ii6y"{ߜ=~UR>rfq#WtN4?{Q+*FHIT$B^/jPwҫ62kBh-v"fze jCQ PYV00ۙr2OuSQ`[:dh5u# @ ,Re$_giΚMܫL^\3CLX ( !۝"ub fZԨZ&!o%!Ӊ}Q TRL}b*[91ASS(ؒ\8e!aQe$C'(KM]bC I*Vf]bQp0UzCHrFՉ54YQKȒDCp&!_%Aۉ`U$]q} TRqV614 M` C dՂZ=)3䥤vP"נ2FF7VU╝%Yly ݕT[g(8Qkg%rrFˆj))$,Hhb9޵35;$YdL]#d*X,s"iSS^DrD}*|H$rJL+]PP@ /DZ141L 똦Q٨~:a0wke[]зeEV$ Y~e$IBQ7$׸@#W!pD(h#ӣZdu"2I/|5SDNL,*؎Ta!X,b}dGF[\U{ (r; tW/eIY ZI[lWM:*hYf;RY`Ajwƙ_py씰8ܤ,Je IsO9I ֘yK'i)u8=dNN)eWH*OHGk>W>S쵏3k g] Xu&v\d56֚,=>@ Ōq4M*Ϣ@Ho71A(T-"`cڿ:B6 *ԳSUdS'[:n2c[Opɞyo];&+8@ Sbb`kKԾFQXTټNV)gocT{J^l4DXeYqBu6h:qD]ǯ8]NJӟj(qxEy и#kuXY#5}vއR(! ټu;lcK~e唔cbLj1NnZ$_%rDG?Xc@&9a 2E @ j\}ag#|Y2|z٘ThG4K"bSX )+`ҕ,\KV۳w{c6 ǐ}חd˶K,.@bW#}!W.@nMg 6qۏ(IDb+IRkuyH p"4#{ 2ZLLdInPRHԍ=F`@e9kl~-yח3Ɯ©Nj-"C`Ă-ۛf2 _>т88N$!yIG,{L-v|0Gсb f~;-Zk7 L>L20}W 8 $X~c="@ h~YҮۏ$`I %X ؜_9 _ǟMYy;Nm%IX}%2dww@DdY0zW}:9i!ڧ^rbǍOdd9sdއ($~1l/769 x鍩l/hKP_P)ڶ_|/:nD}پNN$"s{CVȮȚAyK9Z| <ō> XCܸ êa/c=@ hwR;IL,rzɨI,RuN~X/p^ CY׈t#n]A(,Nh+9y=7Ys(+O\?llA [G/GYu6^XBeOˣQyӲm*nt\oh!Y @ 8Yt!=Z_]bva˷K,/@ քq8lVdߺq7vUJF'{n: @ 8Y؉ƌA_"cJlVXm Fbe{3]Q0][kx#bݟGKq"',6pɊf4%0@2tjm|KVas~[$4͹ NX~%ic=%G.0Ж3l7Tgo<+W9h $' ex:nW˭G2GVr.q]crYubc@pLټu;~Իs|#/iZl޺"7Sojo}ݐi-a?JI)29I3@$:gI kMqgez>DL W3L D%j-&[,׺D2eqڸ .#aF#[FX lܲIMd$ LTtIŠʫJIXL*Vrc\BEbpo&}{ˤ4S|$F2) a(i*wOPiLw!]zFD : '$dTRf]%Iuig@}8cK~AKhL 33@Kh,7i g1YQ*C:3%w$rE&S1ٷûq2%Vj|W$qp&.8vxdA&3"OedkP5VG[I9}LBOhls ':ߔ˜G$ ʫ-ty <re4f0~ey*C%*t-=5YJT;@gfuJCwkL4jMO4h)5wDT HWzow\>q }>z<|L^_!9!.Bb w<_RG.%t۠o/vXOvqtm[i҈_O1xeS =.s8mXv׺Lr%Exaj/54ʇ\:A0^cU7I3>CFSQF}x_8!]%?8xL~P!`ɾ8Z7M1>pm`d|.[:v`A:JG) !"i|w]%2NӴi+pC !R2k=^d|V=dh:&fpp*VYn $X >P$% KkRld]G.dS{ . uNbrOP8O- XƈRfM غdM*DH0 L9qeQIA GhC^To ܉0YH@YU+-`8M4f㿟qMɲ %5Y}t>.:[^+,ie^L炾P.3{7lN 6LGj ͡Xj ȕ|jpOXRݛIe՚rfKq 2~=!RJL)X+Ζ|+O@ GaA]-wqu17"b!$k$-%>A\hh2N[ RڛZP>\-n!D5߀~X# \HRR8%5eD'ʊmQw,y]h r,ۍirHb >='iT7bsuDlB8Rɴ#m+ ,+ K)SJFЀB)*Yfwܚ#P֭:2A 6%Ŀ7O/a_¦ڀ51H|y>%瘓+4kSTJEؘ銯k\7^zaogg1 XPYdsuvYL8Kۤ8e NI IDATDeYXû)Aa;K0tRRːzʻbh!oZDr2sRvY-)3uP`3 1Z7h_Hw LaoPtGIrABquQS(߷WK 4xR5 4MBis.}J4lb_dڶ88_旼`jLRiZ|֢w \xCJPH4et uuς\\3X@ӡ-ۿ:eNz%%uXŹLw[(s102ML[%qQvnU ~fYm2`TAFqx|B kgiHv[vl٧Lcn N蚞9{W8uN8Wbt\n]%+?8OnߚBP|rqA˖+[%,ci vryV&g~+1Wmv2-~3-7}oqKrP,RDؾ?KžzOѳp7{C8Ha*vʡWF ߧU^S&-бBʍ)⧵Vls[rJ)4Gil_aMpbZ:V&P%9= bQ 7νoF7[A$f~9nctslj^?bnIfNػ27?>h1581Fy珕l(w[gֲXIrxvmDJ\}`I#{(绹"K:T٦m<ѧxxr$jqinwoenɅ4%U;M|V6Ó'<:D8$ -)Jn~M´LŸq4*WGCw?ǹ1xZiR;1_(?Ip^>b )YNY̜ӽ8 Y6{QoR©Oײ?W;(3AK,{'㄁7~2gc;W|ĬX/c8a x_7vosWS–-[k6pu_8 AaɄ;QoB&mYqß~c蛶|^/^,Yv[mL BVӴ`vW&j;ږn '=YLJ:t\ {4?{V(;?'W8?(^h:vS_1q\X6vҽذc!ס7yLP(:cƌ1cF5TXqآyZ; y]#O1B2,ufi IR"8 :3)4$N(ӽ8ݔNn;@s3 71rR =7)aspN t="7\vh`ogWy~gl(7_?'҅T.o,˹2| 1sֹetqmdݜg׫o\w$ձMӻtYo_yt}.ۏjHN]=SHn͗bzdrxu\ʸq%'04 ;o2Br!Fs^>c]{pco\;04+2?zł˾7ޒ^%[V~vĸ&; JnQ2/^J+#gfۥ4)!a,RK)1 #e!NXh1: ܰ<~o՗+n7= ݗ4(9,&t/8r7>pa{\lxO-NIDg_n禧Q0.̾;zlIW]7rߡhݙ/30$iޟwG}i?B~9..i4k'Kyy<ٻ5tЏ~]qS͜{78N.gs?qlp\~ )Gذ@QXX@qa^ km*8{ȷy95#C$:LbF9+M>|_ ?}B8(4hCVՆNj֮p'n&~ Hdm*4 M~DFM0 #o' G«u5SjB˺m=nPL-^(AdQ%(LK .޽vqןeZ6%y_7\5vcs~3.'Qξ_~iݸGwiD r<H2>$] >eQ5޼7q9>';ğ]qިuu,7X-Cnksu2H_a”cYr-Ͽ:ni2q_f PtSH鯬 Akn ք9$tcN$-RTvFI8$ɧF25Yⷭ_|m. .払f۝L/{{ x,R˾]ٰ+LIe!j3.&l( uzONNt'U㓮֒;j e++g` ;d\cƹҩ)\r$՘F>)#d«3,s^P(dv F2\=I;[ɬЉ'ɬTLѩel‰-$fb-l{`ےrHmR&ۂ7=\9bwe[khI~j=(w%Ϩ~6FwUfǕb_|EJ'_o\Dx?>ڒBE]O/; 3MkVɜ5̗Уk~8dD}R).-D{cf9 q7V&$Xkax]"i葜28&'^)rI&Ks I ELܐJ=+ pO{o/se*>)T3]3:-p`Q쓙LN)m~Mm'k,[Rkm{KUxZfeKۤxĶY7rˇs7.A\#/ɗ} [X ~r׭LON`4?/q'~q͛04q_eu_r 7?W5#nM罃׭at |!qutU\:{͝?~`KBq0i1eXʆae|h /#vz4Ex?Ʈ;nH' lE-"Pm-%)%)m:RWP|iI0h:Ӣ+}ydZzOg%pXSӷm~hslG: .AJ mqN1{;E]hH,XCuD'7/2.9|;}PT DK>x%}J0_'x ™&ߧ},\Z̖5ڙ%Q&\3 o:p|j Kv-X-߽uHCh-Rq?G%ty=7rN8 8_DbP :B|i%% oS84%plP> Nz2F&Oa徥.|H7P??B8l9nMS2%mr2:5mDN\;_5 F¶̥Vֆ¶bN=GQE83O_,p"S49 O'./l}Df#KP( @"X;YmwGI mD2Ҕᱜ9Կ Ko2u#[t WjYbYiY`o<L'iVKmm[rxw[g9M_ r|jH(T XP( aHs@%9;9Ke$rzUv/;NdGҫ,αS]4G`NKcDpM+pckꂜ\BVe,nxeoV&+Ò7ғ)vSK-iK8Kn BP(V /}M]-BYG~Ȑ ݲuaX@֠x. ۟1 7.E9p66V1KXq]!voMKz BjҒl!Iwg%"EʲitR:z%\5MK[aߛdnu_?ŹKӖK)Akm[P9X:ߴ/z)2f&P7 ̙[F{IJLufY<6w 鹩~f-O>999 qy'CxFw+o2{_hayqF/X5,~y^ޝCn*ard%Y1 x@=D5Ԭ}Ro%<=__s1t {z1CNf@tvY=d3õepe0*̽KQ29DiYH '7~]+p´۹3XQ1 )lm:"]K)k!ӎBP(d kyߖiZb6جqVR'Yv47!ڝ1捴NrkjMvrᘮE[}hOm &@ۄVC~>޲E/~25C-+y|V' e.下Ne|@V?O.!py}O?)!VޮL9c:S\̓.esӥlMI»ln }8m{sQm \ oፗa^]XۋϘƄN$^_ϊ8BHpa㻙,li ]ttu)O a C3(!l+i/aID>ԑtm8㸪 яea2M[*6l ydrˊ{pUQP???fpňJfBt:fU ˌoN"]FǗX_ؓAOlK :Z% rp~E\ѷxKcB'֝{-dBP(ulTd_~JIZoS lAk[wCKw*)AhAն+=}\0>(.47T(GsOYx"ױj^b% vZ"`v1T9uBn䥧\1|<{ZN3{ET}ܘ!򽓹dY<9s!}.H&^ySsmd>f-<6^`;oat]WFO$1H E\t|yjQ/zO/ҩ27j[ IDATE2ȪgA|\?n}+fS|)Uެ:NO>ux%/`eaƫY6N{;/s KwI)%l4LZ*59x%Oֲ&(]\ Dwl]ң;5#l b$EAK[|H0[׳|ka#@>}߾fY5lr|{23tYk)|0ODbmn#HUůǥw5+xdFExUƔnއX7*ƞYN;LLMx>c63!Y3f:Gyabj1r<#:UGҞZ:9ݞ3'oR˄MZBXJ 0yxt0+ BPh@lqͱ ui G#C8²R7i5شGm ! ٓzc~Ϟmfs^,o:72! '(=]6eZ3Lm~/n &`On ^r=z$&qKK%e "*sL/I$ưL=z!jTUʌSg ]DOKn@Pn`HlG`͛$Ro㥆Lh" s0!_%lr{$XFz59d)u-q,!N??$.Ap:E ǝZoU45pYlɆNkYq}xC7 BP(_mt]'7On@ `0D]0D$E$.'DLZ"ؒ k^Q4 de|cileFuX9L N)"ЪVï2w!LDP~Yـ:{FyeƲ9~e6jLq.G{eYmWMdݼyqqǜJH,ϟ#ѩGfAI|2;GMy [?_ޗ-Q‘\T-~_-gX˻hu w<¯{Wp-~VcԵ7@?/czrK݃N맞NWqz)@>\J|t4,~Tp6e^a?]Wϐ9<hcN@R*͸_Wy?/헇8,&~uO[~My4T6hCA~~P( Bq8v().QU]C8AhizOizlY-I76yqeùĺ2Gc\T+īoS2*7Z1S'p8U?@nCc4M$Nzܼ# (@>v=)"ej'7&?Mt_h\:6(O'X" 0eK_37ͧS~.2^+͛K1i;VF JW>Igp_"?/@qQ&P( B8t*)Ftkjt0MR` YpOH"t:*:cA#GaqX÷WCf+(;j0.׈e zV6ӗzK=C&&TeҜ^?9"_Dž=?Av"7ׇ.ںH'Qܹ>܆ں0=_fɚؿ^Ko!>yͭVoRE BP(GQ\T@UM-C'^b7m ز~ZokNz&צt́oSÌ-ul^ò]uD,;Klpj>lѥ4W@&y먖_g9ӽ 2g-mx*A.-\HM.wv"U$S#,$mp[ Dw.5&}tůwl^Blsvr&?ۍ+VGE `&ǯ#Dv~Tx[S vch鱿Ɍ)D",)e*2-r*P( Bq$kŅL+^/u =_LSBHظq#Xh֞\ " FwE5im=t8Swg}38en;Ics)wI-X̨ı]!#3t$wYIJlϖ0?]W'OP( HE5 M‘Bӑk mVAͩ+t$#(uA–ANnnH}u\ %X qiɱbD'U$ ֮$&q9 ƨ azshXqԅ"3pH7p"mPV1xtѰ`-5q7 #3c;Ҍގ}HMۥ',3nܺɺKr(D: g~e,Bu I]op i[.u}mg,F (Ƣn|C݌=c7M# t8~W呓eWtOEln|Gr, P\n37>?/&8 /5K;iYU( B]HR EiNYz3S_5WYG~ @$1\UjUWH6ω%@"Wڱ0NdE^ #'*P( BE% P$2>[웷lnI W +N0&ON9s VSe(^5M:]I:ܟ-2Mv)o OdUҫk{sl'PP( HLQ1TVU ]ф@u4MK8ݠ5!h3\P?'Aތ˟O ŗ&Βö _];fq7fv'J}>ns@BP( Wx5k>zޘÜU;W$@3ƨ%nH,bB7#, !in*(PLYM4M t*.jd^^MpP~r~Yl|},/?^:o+ BP(_l sNIQۡ4e|rw7-͒S쭨cua XKY Ef+NFZ`md]=)߮l\xS|]' BP(w*<+nzxN|i6n = 2Nz'+۾+_)>յ&LuJ|eYi$/ AJIT[ey RŞ:A~q>n]t]х\dm^Kjhz+b+ BP|)gCsWQ/O^5ԗIeҩwY\/~F[R܆A<GUtzD6N)Î8Db,l? ͕Ȋr铛;~;9߁T( B8"yUp]qٙ7-a YoŜ&s4xeza ^Dw'0!~``J\V$b|KD0p:zXnV=#Ͼ=hhqODl+WtF{ymJV̑BqfGι];lK;Zkntz3;kvWN~@q}8dgsVVi{&}ldˁ_ ˮʇ>|=*237%j{?%~nS^.ĢQeckbW[v۳TI{vhc4{4$O \;J4f3pK<%f˕Xھyώ9'.B+uŭRȂeK̊+ؒpd/kx7`70b:xʌ@h\Sfq)#Eg1eyK,K(?}b6}{yeŋ4tϒgS7tlym4y,r.>`?Me:ٹv5X Q^T/nBvy55ir9tv leJ:GaP%SYt!# e#+WmQ(rKeP8P{u\2Л+Yv Qe!W]1 rA+Ed jzQE5>py{J8EaK,bRYH!8lՁ Ms+YԢzn~<`ffT|s[¹E ] /$sZi0ktAUQ UH,8s7] !Yt~ZlgsΝZCA:fNj/qRn[6s>Ǻ,[P2.{2ʛ}\zYljʒ^bCk%ݴ"Eߞu<ơhJQŌPPDē/lajƒW7GZy5ĮUykkj8^_V3?">w]GqlGs52 T<=tBJB n;ĩ^OuPT1ӧSJa;miR1A{GHU=H[|B!{19eVCKOQ:#xOc:B,=Xn% n~^ֲ?cpc@BatRj61P$40LQdJuο &9V'6h(m8D8ET PwVzhBc&1±D?+@ЧKU3OK.&7*8gbooa<mݜEB!ĻL .I9lxb%b@h:*6xʦ-}PP%t~'xmYMIDATY{5ז1Ǽé)uZV>t Ƈh!MY 4eh &ϷLW>𒛹j`D<䶅Bq" W~sOo$9*oæi38wj%[5_Wqܡх,,ZcYJ),QZ'&S-'RBF\&G@'s\]Ų)PDEk{4*- ?ZѣݍGqs)owY`7a rhߠr)|eǐ?)#V_Lgu,"2(ͪ. Ji@6;E[TP.CR.(""B^~>?K_~XG1~cd~ȟ}{E_ER^q}>ԫ0Q6P,qQS3;Пï07|OӬRŋ6}Q 9 Upߚy" 7)ָ6t1vfTyg^o,b-eԼB!FHu~/o~]oNcBI?]ش~%?m%,=(:=۶%Yg]f\xGo7}>_j cwC7P!DTzht?GG K?1?OOr݄WtZW|g1Ls8" B!ҥ\G䑙VoG$FYuSq_}O-Vl5ayfեcиzcϲz]\9!&<͎)2r<m=.pq^Ǣ -!h20<(h{z).*$@-|+.39sbwnb;9lMc_<"F3}þ~U!xo3all[?[p8 ʘ4{ˮ}?7\TK0N۶FhQ:Ucn/YPj[53g05tܢy/Vx^K>箞D!`xW̾8>ATq8?59y*||vNN+k~w?؅Q䎻n*%Y)9c \Z)i.n; ;M?^/~zybEʩ|`x B!|̸x-| UGF<H\ Ԉ@ UvFxCChHE>HMƗ+=2G?i1^xh9KϷtVU_=ؿy~P9헾ͮ÷z>ȢkQ+#ۯ,aZMp/a ۂӹxlpDcθ M]MygSSLYv/%l|L?RA.ZŲmaæ&ΞL)<&{G[ !F婓J>q (mi# !BQx~=f^CfJ zΉPXRImj‰غx!~{_s!Rd⸱^(4@O—.-gBX J&뺴wF`>B!8lۦ/Qzs R:sl`燪}DNf]w^hv:PT#Qr.|G+?/3)O|ڊ".f0' X.N`"2 ?o҃KԔs?Ύo,k]?YIH|Q!i.*~ϵe.JiE+I$B[bLe9uiB!bT:\|AKC 1 577).KNRb01xy(+- X!, ~|:=Wӏee D|k6Q> J_FQ1 D({yʆ E1 (1(|y&㤗\s(+#9B!m3sRĆ85{ !ĩKuV x, :yGzR e"B!hFŶm|>_V/ dw1Fhicz:K(z]BwPhf|w(1Wb m<Er\_2YN&xtB0Լ_4/QڲKbJZA7OO̟_O}3SN wqLl/~ה׸sdԅBA- Ty"Κrw/H,e2yZJB+2^ryv&X[V8, e<<ϤK`H$( 8uT\/rm7ra ף \x _tÏJ`!w-<#cIU~gpì~݂e9l[+3q |ܲpxQ:'7knan&sNy)b7s ^Eu̮Wд1,厏 xc{GS{y:GQξ[9\Zǜ*.i1;W.b:VB!xf.]/&`Ϫx{USt+Pwxqj^ladKf$ot7ٚ}L/R6s,Vn6 O]Bmյ-d,!-B :Pon8y#bJN*f`ȳQ /{ ^"Me0k| E)o=uu#gg}5ba1ClET*۲m}aZIggfup+񋣷MLucغqP Nv&p{]Ѱz!BkK/ۛޱ`7s7cBQ+-ұ`lYP$+@38Mɩl; BD|ꓬs#?_3̳+pcVBua#dYil`tH쾛;ک;qD9q:ʩfյM"̜9eq#̭JmO!u2ǡ׹3= !FcXERwSsUF7 & !NZ0L8>.sLY(c|K4w-6L-BkΖ6L J X{ykzέ{N⓵hv]ZAO%~G__w5 B!Yk \ + g|Ap*[;f[NeSr*`okwfg v!_\Tiۼ)9.*hz掱b_3cח5o<7M/NgWAWSNlcq]5sS:1恟sueễO&w3'BC-\&_N=d2WBhnn&R\vܢ(6Pj'zy3;qbY%ad N<}m6Q 'cOƉA@0F;i&j?Jnsס׫))+"hz9QVU$B1d`{2cnps$B';me/(vu2d0A B!Ļۉ)J~v?m EgE2 $ >x_9G( BqN]Dmgw(ٰyˮî#[< } Ғ$-tEXtCreation TimeWed 01 May 2019 11:44:14 AM CST* IDATxw|]gas]ڒmy#3! d@H)P~]CPVVhCYdL'q[[Jڶ}^~,sYfN1f*"""""""k};?3sm """"""r,ns~EDDDDDXgM`EDDDDDdLP1AXDDDDDD`EDDDDDdLP19Rϻ.s>'ر};^]lدH$pt:vP*++?UW^9Dw(""22JJq+**֖~W-""">'>1jƍc͚4562o8ro[TTTq&~_W?>w>>p-7sϽ8پ}|XpUUUK[}ZDDd4555qՒYrՕL㚫p_# CMƎ;kE'nՄIKEDD\pTWWsϥvm8pPc },XpaqF͛駿+Vpk,\e=GScgyӦM'KW|s9Yf-'O=ͬ3IRyݼ+[_!=ٳf}6oU?-[ؼy Lo:f֯@<K.a<裣=l6˳˖h"M)¹:nUKwN/> ͝˻.wv:CewZ?ȑWuӦqW2V`-$""rмs tk-MMM}\^VF<E]lwݼk-;w`i aKK^a '8˞{/?lrWN]DDHV^^G^]jlJݴi\s}^s_&v܉z̞= c;w6lN# Cl߾_zS-^ó˞cɴ_/Ȯ];w߸Z֬]qg~Ⱦ}ؼe3 .೟ /z[{s))Ip3fK.;os~_?,g?v[~^kHKZEDDn멫c괩l޲+[__OCCO;;vp͵_g|λ&JiӦZJ)|yYO矇yZ|o6{yqyZg-<9l֭_kvh7.~_[;2aF!Qsb۶!af։j@"tif+FvDDDYecnEEaگ Q ]I]EDDDDDdLP1AcEDDDDDdLP """"""2&(Ș,""""""c """"""2&(Ș`ZXDF֭[1D-""""""2&(Ș,""""""c """"""2&(Ș,""""""c """"""2&(Ș Z;e[1 o=[ֱgZvo\LJҊj%TMETNŤ'[Vl=," "G^ggسiNl>OSCU5qcєr>˄G ""rS9:(Vu#A8+V6E#1k:p6q]|ϣ"Y$'*]86)'됏r84-F9C&~s%eWwBDDY+yEf&d1ʭbCGjÐ KpNS5čk Rq;.0pTY0FA8YnfΝ{.dXWSP;BZE< "qn.rLr<{ir*c=Kڎ f)/bqq U!,|8 8Jp8lnD.rض1!=Ӟ˃Ar},n`j$'cTr5v=_p.""r$^OT0@Z!Xd ~/ܶV `p=x6}ꔂWl]+$bqJB75ə("y"kqd k)'q\bq] =~MF\ƹ iskH%Ȃ CdCAZKEqĐ}O4 pǑ "#5=+MdJN]S~X0d,NY,UdqYuwBpގȨo۴ilc|[;Vm,y`8XEu18T R3***ROHqq .x\%bʓ$*KH}LN߾W_~y8E"""j?uE0zc)>|nQYI e ?µye{[mzoEc6+Ҽaxh%M6 "KpFgRNm'c"Bc@<#JQVZJyi)eU,ySR3i)8c} SgNɡD8N_66">lCK=َhc`?ufMdڟe/ϩfvo{hrX 0.0-JRIFLƦf-qr6Oҏon%D@K:[#ݜ!hSf `q%X97dϩ1""28s "Ckp@Mϗ [/mqƗS>3AG!!x=J&q384ox^LKntN; f;6'XZ9LMFԿz)g\V1 &vO@΂Wq⺒Wmdne6"YgǙ3K!C)Ƨ*E 8 &Uжt9~u-Da8Z} !lk-,M NiKㄡCc!. *S#/8q,|>O><܆VV9"U 6CEMq Lz=K9c>=&q൥<_?_zE,p7a@۞'{yy N~C<$kZ:f ue\0e+ث^nj V=O~'N䉼߁Lq%U,Nr~TR5 hmk|F!k'Q2"IDql郳H6W'sDaB d 쾆!yF6c[i(?0և"CgAm_’?jl_}WeZ:>7S;bʙQ%^Z/fVEȞ[ \jACնy'^NOPQ"L`IS4D}|5YԴ\k 8s8qV%q&\Bk& C藑5T6w4 Ck[٤b &:-aSHF|uAaccܼ h"UYI͜:Z5a%ϡ݆W"ϡ=_OQǬюK:#湸?$)""2:wC`1W.$W] *gYW{2<]p \YR38)&&: -;Yi| wɛȴeiwl{d( IiI/Ɩ@H=*!ɭaΛkhVu}>F l^Yz϶zvC( Hq|!",cw=rq u N4Ibj _߂È,FZi$4QS|&"Hq]cf(dBhi0ݕs@BPa`S;*_uwulY0^ ~tP2YW<3{'s)xI5v6X&%ǽ1_Y6=&OB/̓ >,~'ʤYHI7$ʇyH2b,3qfpBhp"0%CZ("B!1$S'ElۏyT̚hJҾdxR %%IZ[ɶr|($BԠWDDd8T'W+@Bbx~ҟ_8)^ɜ|GoW~74o/|[ܱ=?7`(-#}%nj&o-iܾJ(/)aB8_cCԺ]KC1# %lͦ^AP>e* Y3Œ:0Mz%۸}mmZ:e8/~q/I\34N#Ȃ5s9ڳ6,A-md]IJgLV] oLLٍ9vg8* Q9%"!ӧEDDF@WT"7-].3ċ.䪆4|߸mۃ|uW YK7TM88g2Sz&g7t.O*-%ac1d# CF_?Md,a>d˯=N`pDGh ˳kn6",j}ӂ?p+T yx83m䣈o My"MD9yryifiڶ{IUPU[Mg mī+VuN%cOq&'_Hլ:EDioo2}MEQc5X-"GX iC ~gslHDD+ IDATbXCuiho",-MJIads6J,ċ'(aOc+W-MWFyBqڇ~;.ZXDDQL-&T=;Z!XYo "WO{;( ,'HGKd-jg8)`)-I:.^e3dBK}zw>wƏ U^͍w.Ǯ^"""e{k-}L5aU!Xw "Cݾu-SQSma 7E: F(Qwpt,{ odc]VN7*(IuO%ضx@&ȓH3mD3&ҋΡI3g|֜?ŬOgۙ:ɵDDD )& $xcz\9g1؟2)/2V HPOm9BN|N <?ʾ={bsigɆSOZ,mMVVij Y^M)LƋ' ^MDD .]+T m8 ptuVi,RPUn+Y˼ d3Xc(%Ix1xqU̚?w9nB%̡v2eHWy5k 9=MTu&otES[b\WѪ7qU`<ȑݻӳp3F!00-0ԕG,tJV^c}m9Tu1_w?ezCy>,~=7e0c<7MM}~~.~<{Kޝ{韼ww |I^O~3yu=B'3у~ybwӦXpDB{!"""2:Cdg5w7qu{Oϐ ˶5O_OL=d&g̵\ @v'ϯJ3{L{?*X>.X5v_G.al=|/WE~S52TzMaxu6DDDdS׺qC5 ̐`cNx+*'aZlc=Ma'$r%볐ٶעٜ1;hw5 ^w_b`Cn,.{/`ߥС2SL']õ/\[Rv~͓$q*91.Y6ϫ`r<2> {8)O]EDDFW޺E*ߵ "G;c.B' lX؆y;6Gma0Fy*Fǩ,ٶZe ?a,nɓ7>qW?ҳBnOխu']Euuu< [89Oׯ^w ]_w/_h{}T`wsXf#-o ;|^7QJUKʉ7FEEUcˈso3F =j9][y][;X"24skO_K"uo$"""#kN]l,̪k*bfz~_w{b)D!ZH[ B*l ck޷}[а{3m$۸&6J**)7:k먞2IFEDd >u݂]йZ]E,2 }w[ onWG][RwNؾ;Xvn#d9q - a%-n)sNeqpwR3eh?:g5+| u{-"3@}^gb$W]9s]aYVMd#Ḳ47%YQ9!N, d͛[ȶd ӱN1Z"k k `ډs8wS;wh?6n܈1uh6hmLͅf.4St9: L 2ysmSҭq=ca⌖FnnvlDƧ1͘xH,0Iڽ,Xt"K`*; ֭dӆ5Ys/(q6~d9j^o W-"CC]EF@_aZF}~}+7?ڝk,i aB' iuZ i؟g_}\.XT*<ҜӸdgZ]&o8/G[&g^ Xc.g1o}mW 2 $8sLfx!by$zyW}q#Kڋ0D@i&Hˎ%*+*sԎK27 ~C K*vȚ[ҭ\7@DD][h?S(÷WSn/nw]^̳3?OΤ<]9^ECrf]оWyh~B&.|MR "ғuqݞv=b[zZDሌBXiһY/)Mq`(>1bF0 $IbcIReY/OR`=̳=F{RO8&"lT zQ޽{wC"}hgwppGOkŕ6y! Jȇ!aϫf.?x6;O䭜Ye_W~j39Ws/cc2mS+""ǐݡ bUѥ,2@m.LG[(~evi9e /m'2jc0XlARYPZl̈(q<<`@M0!V 0%"b9,c4diϑYl>%o5gL͋;%J9rf-wOʁWђqW-7>'GvDDFPo!:W!ZdEF`?HX_^S}P#b[!:1Rc-(r$8s$%QxW0o !a9:1h}&LaBVȲe&nF;ʡX"aUyϏx_ĆxeWs 6m汇aK&sڹjGt^2_.'l%?/Ǜr?~(u_9r%g{7߹k&+^4̍+>;""Ǟl1-"-2rE0P8{ʕKpKT$xG1T `qYRѳ9P) `,ω1hvsnri|~{z#}}kۃ}ʅ<[ϾKL+c#-y1MlLZrk8w.L+C\̏nO>UoW[&0YDdi }} ՚½M5덆^f8k|Jbq7  M2bɵzn'q;kysfPk7Kw7V[.,Xr4'3+}h8a)73BXxx9f7p7/y5~3ݴ)} ߸n'2Yݿ9pڄCy)ϬɚKDƞ4w7T]rEGXdgb) ߷疽ubM54hsND #rA@. <'*dR 6<،j;&Cצמ\/Mp5..(Ǯ~_~^WQQeA.n\楽mǝ:y|V=xN[{_M,Fh ؈[EDu .$"oKQ:MO>A&#f|39Ͷ%oa0ӘigGK -F{F(uWek_!:璈ʼn L\x=osYor_x+jyHGz/r(dd20۶f1ơSQQI2 v9j t٣O$ lidO@k:Zm-l.G:& #tFV1x N@II `1DE6n`:}Mg%Cqp]X*A%X L7bBY9L_AQt6"V?QVǐ?poN;/{7g.! 5g1yͻ^g\.r {8c_7̧?,_cw ?x >l:S ;=傞vIñ, ް0 hkkcϞڽz d2=d3b1 먭e)TWP^^Kc|`=O\6N;M<0u=DǺ6"B:JJW "r^|.K$  At;Uxu()/.~,F6y&o\ 5y|v6_Ϭؿo 4d,#gZ/^6yb׸[U;z%f7O&:|ڋ˹/;f%عj-;>ǟ6OJAR.w|wVZ{޶V;UkZں‚$B2N( `%'9|}>sw#/vOW88C":?<ykIS 2jJMh[/\MwB|0@+<#bΜxٸq-DAXu9Zt:…xWXz\ǑQ!vy(eѦ(n=G8M  DF""Qc%ĽuT$ڿj`>eh(@ CDqXY$DW6 ܼr]\#nYُT.ˈ@MkR Ç4P49ι?ۜ[Z<0u]|~ )l]c/a.Ǥ53}ߚ>_f,j-cGn ktAN|nL#9[} 41 -Ja`:{cK_y)_z#|Fj mC3SqRMi=Bf!޴ϟ^?w>=_\4/4ۛȘ1c'öm躎iGp,&ٕd r)+`i 4Hz7U~~?۶nO'Iݏ<Ý}o>vi1th<\)44H5UTĢ Be9:ٲy+m )dB! h`-"-;ٴ};AӇDB~BBF*EYܼX5s \Iޭ'zW,幇G IDATB3L];K.MֈQ1a1ۓn>Eg#io{.@+{]NXJ7 ]绶q"cS=ϣ fJG4T M&tet"(χyض8t45m/'a#c;͚9 hQ5F!TEk_1cһ朣!U jÎa!g⚿PLA)2k$EHC~t;E)E.'ґEV@2%` b,:VFjOtz%vN2h>l]OtsP3K |F0;^9@;5st?4ﯿN9,e.ao!bXlX,LHH$`U]B@6P(вg} .x<ZrYLݠM edyBzOg!W6DZپm J9TyhDp8RBtm=sr9-d2 ]'ˑNgdvћ}[z+t9^燚~u39_Nq..N3 9!2-/0 844˟{y*ZٜOgxz}?TRԼ{-[td){u,8^ER,; 8B'C0ëc(CQJa;yS0} q| 5$'(`JJ*#dwY6TZ"Dj:aDZ2imSJal|>~$oBzPB!8^ ˲-R<@1h4Mlu벣VׂRtv&ID"a Ueq]8 *^aj [7nC7aFe3@c%L;gzW/,pP |R4^W. @\j:F޲ m#q\P>Cql P 6mD`,'qq#Xv:WEr}b%|4 kݣ9E^yi1痓u ٿ0bdi]L2K>[ Rb2 fJ#c.pBtv]iJ)`MѾ}B!0X4tu%)XsNRi<$ E&宻~Ǧ)I$hmmPׯ?Nã₊s!fd2{'Vw)~NA_qLJWXaߏima)p\e)yhh蚎q]U Mr< BIb bM MS LQ~@5AVAց[vs[*d9B|_ihxB!đ3d4m߁eYq|)IJH$Jes3fN.'1qx:tǟ磱T*ý4G|0T}ޒc>sYK:gpZv3j8vJ`[M3傦ch:(E`g)|8^1<~A0u ,.$ Ӂ">pEzgJi=|:v,L [1uu){2X!XpI"!~4;  %HbСvi36m4Ţ=)JKK)++%Hсe٠i\veD+ ~ 11/q>WQId֫mǸdgpM'GW&+'#8֌x=x\L =K6Rx@3J!Ba|>*L]]-gذWͶmM ϰZR4(U߇y$Ioy"551W'YӍwu#4xkEh`M 6_x'*$ǚc1l^&Lk @p\ez.ݡͯp SH4 34>T׶1TB)?@Ӌ*UZmgk;*o P(ƣ`;TIVvw2CW)mǸLB!8 z= Rh?xeRHT*i>xeY8m;tw'Yvͭvhڱ&ˮ|vέ8-˪8ßb?Ctl|Fqkq[wqFq.p Tt:={mȐW e?۝'qm.?9bY?G ] ֡5ŊnUq|lTTT!B^Z42ZP 9M֬YCgg'---ttG|4OEEPM d2<,jjj_!%4he8K*(ENpb 3U9xCJsHyI4W* ͤ)T6Vs]h(4]G wuY&V73pi=k[?ʘ:#2Kpv$#N?AN=$&st$ _WODlq HNc"K.%c'3ɿ%[r/B!ǺVZ0 N9TJKK_q24t.;4mp8JdDTVV !'f'14yr xBi OMs4s]|NNytwZlB%tR}hsrN(f0xٲm;@ͅ,aE}G8zyrZ*cÓr :hir~gG#osqy4Oc&b'\0GsW|Fnicg0*k^B!ı-F8k~24֭g1tx"8A8ĉ'gӄGȔm>EgWW.z(tM]lCgmwd4o\ISCĢus_x^G&'N3]YDcqNG~Ĝ]JKٴgA62.Doә?nIE;meydb:?>tA?33jhR K! Xɓp¸1Nub 4M6o¦Mٱs% !c.>+2}؞"9(T~up<@3ǎBQwYn [Zvf)X yJ,o(סɡECG9Zr2/Wˇ?z_v˹(6ωA-ac^fٳ'74t@YyrB}dZK*2iuF3}_؁i`c/~ !B.pK.;-[ WT~F_@G{'2J%GYB#ŧ1Vt(ոhgMDA MG*EůJc[K'm]iW$9KHe ƀ1TԏŎv]ȒًP;:)/襑_rZmLbR{(Jf/a^D.3{;5^wvhb7q~OJum]x<{9{=:w=^=b~>4 ie5L!v $- yXKT[;N;|:/$QVFY!XZnCO4 :ϿױHSPG`g1|FDbߣB!9".pp8LeexJH$TWW~Lq슗&8~,5[XM*# 8B 0Mʍ(ma+$4Gdp]U"T^/B!DrD])#LiDH}KԎ a7h\% "(8FS b)ljţL6#S $|?PE5%&vׄB!Q s,=8mmA0$ah(7x y7qZr .duC#d7Tt~e{L;m:R iB!P(ƜضyL&M2EB@ g eP]-^{K !BqͰ<7&5k(+-% NhhldGIB!B!#6uZvli-3Sőᶶ64}N<`rb8`h4ayB!B!#_l>jj1uÎ;MB!BGIe!B!}B!B! B!B  !B!$B!B'H,B!OX!B!D B!B>A`!B!}B!B! Eay IDATB!B ];mB!B!;B!B'H,B!OX!B!D B!B>A`!B!}B!B!=_f2kK0xp=?;<5 L0?B!B;x澇Xlj{Dgݹe5xX|%'ѓjާB!B`:vBxq=t@?X.n`*pttFFr[wq10ߒp]M? !BB&x !^ "ҲxbͶ28鲫w/sG^gT ^-k_l#i^mg E<ݟĖU8^r L wX|m43Cn\| r_\OrCXu&]|=nB!tǣuG}0v"Kr RiStu:G63Ϳ̒B/g5lRZ1j<~?X Mn JW^nic7#>i~|XJc2bҿq˗OcecxW_?Dž`mч}_X =i&Yܻ5^Xؐ +Rd%T(#ӂAJtXIwsi9>~?+yCq\V%B!Bzoa?f^偵?̤c7z]OYgI ϤF;&h%f|`D4_Q~ Y#I)YccצE }W#dB!Bkzm(PMࢼ kSZ28~8곫-Ա@`(Ƈ7ܬͤx<ϭ$ƌb;a:nP K<(Û-g fuܿc!!8CpqyGB!yxwwRosE!xzmx[{u1h-iCµ< t@-W~ ~p\ SF%H4m?b5!jG< _2}c&g]yyaćsյ1|U-x0PFK9o !B'F>ŮqJ[7g J /ȑl/єzoy82|;aeҤ Sq-eu`a7 jC&GHݙʵ1urlpriR*D"l q{)2*H"k]ae:8Ƽo=x<{9{=:w=^\5^!&E #\5Z!BM^o.@c$ !0yB!B'H,B!OX!B!D sB!&AMBRZ9RZ!8zP|0=TKFB!i:r !,B!OX!B!D B!B>A`!B!}B!B! B!B  !B!$B!B'H,B!OX!B!D B!B>A`!B!}B!B! B!B [q\1)-0w? 7qٞi|ޓ>{''}nDOh{/~s}|Z)Rÿ}}Nn==]KSʟb5q>MKVjtܺw Q:8T!B!^ 9(-3ᓩlY O1sXZf\Of a/e8y9Kckny ;e `V6+ѽ~6>8O=˧ $GGT>3+; %:%J_.l0xLΩ{xLʼnrMy!Mi\~0\C+iyMA{/в'M/cN>_f^}E^mlGiBhr5fPd2W`/LΔMg̿6 ƛO*?wC{R}q_`Va|z~5^+P3y<,ia$C`!B!:F7O|7|}!8 kËrlr2e]M/l;9 >csƎckRKxuc^[Q,6xGjga ]i.\:zx\)vd,-\ro) >ڷ&kZ_yegΦOp7ZCew=~ߝ|᛿9Yj$#^~Ń]vκo~7> l8&.nh_?79~ &D62wU:׳1C!B!v`w<__'n~~XQuuUμȭgӚ>u>;;| 7g~Qt$PWrFu='Tfh\<бr! ќ2~,f 6X9aġQB!BI|0B8A3z5ʆ.=\7 ]ׅ@+;_>nõLcFv$Km :y:GK?e P` dcb4*Y|S#ۘ 6G0s9>8n`83mBՄٿ?M5?A՗s=}өVK֝Xщ\uN5O?ϲ[2I^Ύ #=R˛ElA¤v|š} ּ|hR)૧R֦=- ~ĉ#6aaMa\b?):À 03//59~̚Cb#g ~nAj F_|tJ9[p U?n1m\>཮Bfr pZE8[ύKsݷrImS|oR2kx& ,zy)3d~_}D~T!O)4F& s^ŚP.; wxUlI@HP"bo/ PQEDP"- ZHf?@H A}yn\Yfϙ93k9<@ nLY~GԼ62Z!B+Sa \i%SU|[=.P'T [&^/YR1frkмswhA؃Ų]vHyi"wEFLE6Ep}@~ںo[}VL8E8 8N4 \L!_=L<`k=җ}kЫ NK|yPѽO3ƌP. (5~ Sz*|.W ?'gjF[FUɯ}ۋ!ܰ0ꇖLr\Nze}HπJ@flA^~(cKeB!BTT.x,vxJa zNNA fpPiܛ4%XDWjL@m"2˫ ~^5ܳ U+-O m͟^E+uo9EO(Dv߽?%!X]#y F|z#FV8ͼG1+'C<~ :3^T\_+Fq}c MWv?.♮4.o^%Lc&?u GX.[f~›Sڰ뀦5;NӶfOnHnExU'm&}BJV W?뢳eu1&~^sءMAA/3E6!y}6zи|z:^|;Qf0ͤʞfӍ9>FM/a(*j@E5|ISο>͸[-|1ksLXν(! wiQ^=ja$hNa6=Y?1~PM4FA1i(>Z@j{y0Ths/D9GtJbM \G&0ѦE4'.L@݆)1i)B!Bøɺg YlrfezQkVEpKDPn;XD3j7nL^.CJJS7PU|g8]:fBSÅU2pgkǥdK*:  ݅N9+l=' 7<3Q&n.\%bp T̈́.'.CA-0_ӆo>Lqn. R\߅eUE8*<4l[΅B!DUڻw/`2 F) {( VG6p*Zxcwَ.ZǦx.R꭫b, ju\.TT `>l2;JNj.S@hXphJ&7o|.--~rP[~@ŏY4l[T) :W?AT K|_w@^0-w׵FwY2fIʬLڅA2_^OB!tuBLi8&LNƕr=7gY 4CȻ%#Ko3zK!B!T}WaUCQPf3QP,\^(adZjDgf:Q) Q<fj,ɖaD0QB!*T}7c 5ł(kF3 fq+WD&^w3g73iץ'SYsB!BUΓIB U1 4 -0Öf ` S@Br\3CKp"@=Υ_rwsJyB!Um{<暑pge8-,=+c(8+XNfͪio'1ﴅ&Gv-*aqxzX>- Oζ>#9`O[nm;}O6PED3VL*]?V2wLE{dҽ ,T,%)E6ӵ'&@vAd|< ,5{Uyz|pt.cL?OʷS&\]!P;\J`#g~/!y]=iZz/ |1c 3}U'i'8[~_'[:ꆸSp[%GO[~7rϲd5 ?"jeO'!){^e)IHsA?qh8ku kx`Wq7&ݼ/g 탮k`ƽ=w`64 c}qȥ6[1iq{z'-m=I.-_naL1` ^Z~^/N#couIb"SrjH}JÙ| %B!5DsٳGFoaCXU@Qd$q7SՒA0)g6Ի7U:o4 f.x-tOEP 40[o|މk@~+).@5|<"1Zsrof٭/ M1XUiso=3NER#{G2.O-U88{\S oBS?H8u6r&EM ж?v&.gvNj)\\HB XUd?/BQU$.}BRY,]:elC1Tr.ifqjj/ƅK?ߋjљouBT v6EPC0z/©ʛXx]ʝX='nKdݣ .rǓ5Y~HbPߎr?+$~(go %Dqq:$8xBd͇^L& #e7ˏX*] p ^ \\oeEPS8 M{N!zA8l';X87fOb2iyv\hX|V+f-o֞hMwJi3Ef N-L 4^{),`o,!8"kj|5:a)Ъt9ՊUawb@KRNت(dI0@fg#>7y89I9B!H\(mO<=pdX#H2W i׌>N d0rwq_# r(DžAnoq&c< 21f0#Mx{n{~z;~Cؐ]4Μ7[׼i8(ӨZ8Le:pOWkȮϿfSá!ŕ93,6C3:|#M2e*-COzw6Ó=TPJw>9_Yx4&›w~M;׎vDu Q*2􎈤Z^<Ru;9rQx>ȾVdsh&u"KO}4#`#ǶƓZak,\slwyvnyD@n><Ȑh8f& na͹wpgyiƷsa>SկiVg[В|Ԅaw7+lck z9|?A|$r{VEհXݰ*ٿ c䱄sk]*XŮi8ƌOU:q7kj2mn^c:o/X+gxQܸ aY".lwͤф) qr~ Cxm O ^VB!\q`F:7 y#7hB]It_$50V`?Ip~vOdz}^hiaTimC g H`38#Qay3d z_j$M2 NpØ\o~0tR޺ ˉCt:=O?˄˲O tNTDFol)’gܖ^gzyxc%߻1O>ۗ޴ܴB}g_u}9 dլM}ّ yLT W[Xw0pg G^cSkg1ax/}=^C" {gun ƎJ_|^2i1M︇Ѧv'lPp^{3 _򺆝vsr.)x]YWgՖ&E>wYtؿz=vT3fNZznA`|2sT6.Q^3ttNv KYFP_#'o,2]7+Zz%:OVݕŎ%؝_fEÙK>ºB!-pq;]l67ofԩ$%%Ui֓w:)֍`[|"(oG女xZiwN:gm:I9ܒ> =q6kN2]/G8oYj6mN$&76F{6Y70a۲>''#; iә^s]9%hbɌԐl1VnɢM7&wjt ǷgQK6S~ sK~eObQ?lImO+![2mÌ ,xZ.}oA@bkqib-浫Ѹ;N|W.:5sd:kʟh:Y^'aI[Z4F$yFѾv>vIq#X2ضM*lN6r y lԚC vNc[vfѢ_k*pf/W׼T*qk^ |7wvù/0{cI IXu |%~e&^A~8ݶoGVA:q_= 6K3놻y,z#nMCx91Z ǒ/иRJS#{2OOa0NT 'Ne D==Whֹ aĎX b^;y >܇Fob}39~*qN[' B!?U GT.Kxx8O<111WGwp:(ǮmVvvk+(K3N2rKZ[<[iįv45b0&zwO .g:d֩{[Hy6H=<8  |f6[&i67RW !\R28_u{VY&?B·xNj+FDdyغ950]l=EVP Q"A̘}|ܘGhlfHX:a ^4hKԦhvO,)>/n0 d;)3dV[sHk4/etBZ!(,WpM8mT=wߥd[Ҽiؽ/][WG9 6_O ~lTG>ɐTU_|46;Ɋd;Њt?<+|습C&"o~E]!5%ŗ?s#>9 ֢o|& 9e"߂fåk[jxRW!⟫R`MӨ_>ׯݕjMtttؑڭxs$1pFv{i⯲$ps7^;>҂@0@Q0j4p6w=n 4܍p"[tdj"U޻6n2]!'džt=6vpAJ=O]nO@1n1Sݼ5M7 6B9F-b.69DE Qp2OՌw`(>g?XdhSjq+zdݽ7y%(< +ZkE;t57B,xps/']D& ,>A]B!9yg^mnk ){ZxqpI.H#~4>G3gW.EA=+ٓ(4-GkT`JTnGVD Qe{QQurW pl9[9Q?'6gk #aq2mISqSf`{c\Cy/ȑXB/2$K']#/%6_ ռj@1dIT4D'AdVtjŮe84Wߋ7-CGQ 2?moucMSC7 l8Nld;WT%~}%=az& '9Ydde]Ov4&'B!*5 3[PuQD[ϟgRJQ BF&ocS4C#U_:zcaDz7p.{ ,R+B!*bD+ĥYUpbh:C a-ZVwbsl:F'#ہቷDeN0QN'.\su^xS'?+\'V mB3\؜:ٌI)8#7l?wӁS)~,t+^:R W.d.of׽W &IyEuw~}f^EŤ: LEt+b*{ >l2l*^XbÅͮc2Oun~-<7OMu;렟e?rPl~^^rr8p)ϯpаQ _P!BT{( & UU *+"/M)jş/*sE)^2B/`q-|2EKKS^mNRa߮ w(7.d0 bcc/:!H,Ŀ5?XYpċ:[:B!J\s;ѸSY!BQuX!B!5A`!B! B!B\$B!BqMX!B!5A@ !BqexbbbBk6IB!Bk !BqMAB!Bq*%HҙVKFVI[đgdT.!#I#ٵq{^!B!wyўK $G}ݷ$'|̓I޴2cם^̺)<hcٵhwݛڏ Siy}xgaMѯ7[>ݻ_XZ=MHVQ%a|~Zs,Ztكj{#aKܹ+zI@ݶ:xQ3QdY~f%/#5 :oP* qQC 2<ȴh㳧eWs_{haf}A4*,S?e>^7 j4=ĝlI½z#6VEǪ2.Rd_ja}͂op5|2B!e\vɝNVLWʑgbB'wVB(:7vg.[ό bsטylNԟȈ {V|tYʘI9\|L# (^–Nn=sK)(apzhz,?Hr1L~`3,93;P>W'>ONR~l?!ۢPq-[t--qN9'9pJ'E ϕEaɉeJ^|_7GZ%('ͧfV5}$//;xw,8#B!]viZ~-ut%Nl/XsJe/darmzv55[9#':G~/v)>u&_^:g?Wo0ۯ¼?1H[-8Gfޏ^"_=W\j;5U>t23&aO,KB\0bB5m"GX$Jp=PB7+M'QC"Q2N{,m 󠶀&m*ysHw(D/3;\|k؝]eÎb̞ƪ?my%؝:~Н؝s3t/B/qYu6O$z=r2祁zmuV#Aꦯv&}ÄmᥞOѮ-uM;]Ƽxի7 %1Rlx~:Bνhg qr|Gٛ7BO1rY\+y{e:tMn33Xd<}{BN7l(~Mr]-B!4[l'||k`" Squo=WҰMc:Hzཛ gql>n\I{$䓶z47t MM}ᕥqf +9\`"G >ar,z޵~~;oӥkoZ9o% fYFYT9N3c=pK٦7ɱ~G›He~s{;g)Fc3:PXmgqBNLD[ǂO G{wD~csx=1O۾6&29pz<5כ- {qK8[oAz>o{F>1g1hֿ>SW_uX-7c!o~9{X Aѵ$O9{wsxy3 7iÝpkAV-7>Vu3/-#׆lmno,|#7-~nn8:q܈&42o);Tn,yۿ;pr&@&>gJ/ɡҭ7И9caҍ4SqCwm|w<Kf v=QbK7hxҹe0_?I&͐?'B!7p|x5W2X!v7" mEsr%388A.\+ u=&l۞Y/Ѵѵް]*wٌ%f@W6pbzB"=]ցͮa׫KX2VJg@9s/haƢE i?g}7lѿN]bU>G!. ".,\e[w*<fOC"i}clE%8t&\&woj޽-q5,]})&w7̮ld:.CA+H>NoNOzeQrv1~{L;ND<+Q;t2Yiy ¿R?3Cr4;ӎ`VP/8/pNqW4ּLxsID2|iy95̋IJ$t^87#BVM/2Oӫ(?6#BVI;0񕧉LRC'W*i}fr=qK֦;s~s[ӢM̝c3SqfEP?.Ca>M[Tn9-3>dĂ^Ez4Cm0YDk]I#i 5iie޲,~m6#>ۑbk>r:c5 AVKw1Ԭא]#r#v1@A'hٹ=lٿ_Ǐ̈́_7u+g"1D?gs;YNv~(w)K0wSQ(^`a_"58=E|]~Phғz6EYuESKx;_x] k(qmZHc,[f1{iO%4;9i'fwNVV !B T8֖V08LJ\\\YOBۖ϶%kH nˈ8rMnu2skZӣP>qhnޘXK*![yt tfBЮO&l;׻I}WC+&xG5<}r~ ha=@gld;ؤZa૴Gz6ώ{S f< ,_bo0>nmAMc ~:?uKݻrdo<3& ̮*.ON^19D%b>8Qw'J=%*?myc0Jz<']Xu-\37KMdAFeՄ 5#/B@12a-4;OqfMǹV 躍lel΂.L\ĝ6 ݼi-ޚ; јEN-0#_7gX3/RMm`, !uԩFhƆ  !.U+[sWVM@-ēڞ&mf%SxhxBT|@3 mC  BtXy2mh^xFy汏|_&9>>x (Z6qSQr2S=vT Ȼ0HO/M%gEz6ȷFfz67fПuBT ٙY4OhU5Rl=/1jp4WUpyߏٟݟ%hᴿk8cr=ʦr`|qk5yj_k.|< [XQJ U?(Bkӧݾ!DE)Q \oDxi e5ftw;ӧq> !q*\qELV ,|2uǞd/NlV.~hM!B{1w3pf괯>LTt3M=).oW7jU]uڛ]v !Bq^|ſ BT.SBl7 kV!&̢pb<> ͣ(^Dӱ,X<$ B!B]ֺAZ.\΂k ʹPeD"5uYNh)H?9Z 'i';$)xbl?B~EC;s8+6`kbsm$'e>HtKݐpfbOBgl7fx{Os9?k^cm8xgHν]=xu|>_=9YpOɚB!BTeۚi1rfVn{ßٳG|S.q:OnId~2 'OὩ81]>7kc@ljV r&oaOXzy9l=c~ᣉxO9xuz} F6ǓQ sYׄKo wxTU-SIIz H*TRlX*k[ϺJYł(+*@˴{d$$P\̓-{g2sSe~#ϼ`˯|1HR_Ν̞s/`s*+c#!B?Y:rn}kl]ݶ:F9Om,hq|Kf@ (QV2%#.\5j1MGS.+طTg+F40y<PdKKxnt{TO,jӗ q]晁u1J?pK2y4:of]9?LXke7t8]Sif#~.ox "{݂cb)**~gbo,ZC- oZhza`Y= KSMCOp2 zXYļ٤=7B!+v\Nq!|6>_aէ, Bjd Yjmy츠 4O٫VQleԪ_m-M2[Wcb*t22ëbݭOHKGo䡤xrAww_Nc<4Ij6|8O͞ebcf@|9Qx:|"G I;xw|o-b=h|8Ms5do69BΖx?R``k=[6'MBٜzFOb~_4: Yvȋap).oA4Yl3ŵeK9?Y|^-crzyG"~̜=nwKz0 qdZ0'b}t]6lp5'kLY!+I_ϔ)? !BqVn{|VӧZǷXbqېҪ--ױdWe8i&}װܿo ?MB)ߕO_O%xXj!<q݌iFW$`lʕ}=c3LEAi}x1Ρԅ ~\tm=s!?n͠Ijۃ!] ^tA\3͠Q kXqkRəF{w1_\d[\}َ1}n')3p#lw7{oƎyk$e<}ER/f:M]Z u<6mqHzYkb>K>ψ'p0yǻj4nq k|Ow mC X`xy֮~gn8n񘻗7i2FF?Ĝ9ڜkoOW95B!BT`B>DŽ{\he痽1tijHwU 2VJZ^˖kҞDQ)nln'/##FsA誅8l:*`Y]/GxFIւQCǹ7;u^JZǾ|fxޡt63khil:MA r73f |ȡ.D' i[W%33J%Vu욟BB3,@bV}$zw&N Q!؊pe+sMX.s_E6Mb|;a6P}l_479=6(E $u fǛcQ(EQ@fJtdI% !7^/G-RgD!+C<²,,3?fT^%ӏ_JSk*cM?Cfӊ? _A(av-D IDAT03(cu6TmGiq`?G- ;߇F蜲߇ϴͦsL.RbOk|~cWʹOq G]'o'*̆c(:v3@~]0qs[+$BC^|C/UGE^RAANnEWً^[4K]K~w.9^.;*%/$6))ǜB!N4EAuTUEQTU-y( V.xRiZIcI+8Hws$=w~YU2_#1B.3KeC~pEx%M7g߯bzpu$H[طL2R~zzy|Rӷ:g,z v,{8,`!<8  .oW5N֯9_]ߘ>b; ⒦63TdT2Rc>Qfj؉+Y<ۘCD M#Djws 7!Dw#?W>[O _~4Mpx $''SPP}Jt{z9r#GEbb" ծB튞dmuJ^Uex3No= Gט=xZ}|& O^3Džgcu KV$grK&׭g Ɣ9J㑯Iܐ٫๏i{ԳLYq̃;;f2நAN7Eݗ-ˏÄ׳x ;4_>ky񶛹oY/,&^wpl9+tq$;mҩɋxYjK/ˊXn~]'(ḃl8/6ו([,ۖE­=|0&FY}_2;Wf~ZO$++4”i/P ~HwscY|zGk 3xʹ?L}w^~C]ydZ rY0>׺ ~6`ݛ2y>^o]\Ź2 Ɔd{Κ7P^t`qpggp|}/`W9QxؓY@&>~_LH9+ X?~?x-ؿԃ?M˽Sیde|ݟ&DZǗ |MWaݻ*}BVXKj.Ֆ;kO Xp ј~?~?$uE Y> P6po~g(G'/bT =` o<707074?Wkm_8/~MQ=mH2m .KjŬ/e"d.Aq9 эSGbϡB;暖>6iNٞK~xW٫>j my^\4>s1=+rOE?l-?q{ҹLq~?F^.5Y P;I^a9KJ^2o.XF5$eXpM)WGVV !jTe Ta!F{bT1_ӗU4Cya0vpި!3ˍeoO_猾|.&~l/%t"B܁VTAl:KC+nUQ+ÇrQPT 'Fe`a ] UHiTǓ_<Ѫg2YvJ~HAQvOHͯJF62S3ÔGpE琻WF˞fh= ]GN[L7/iߛ1v|с<~~z>rjִ !Kb~;܈NM? sp m]Ja"c4~]?}{w ÇT6- l\ PUj~]5O@q;pSp@D[lWصUaXphMC՜'޸TU!M|d/4i(@h%rڜOGկ8!qNg} + |O8;ۇBQ=UH0#_?$ 3RqW^Ar8<;ֳKKI%zuP?Ay䉗Ch\cz߼`@/fNQ'nh qz8aٞV,c:HH64 fǦ?&͓#L[VeL)Nem~* adbFEI 72<8KFߎk5#$0 7.n@3P+{=!9>bp"u[I\riS]P4&9w%3mHWM?EIgќ^6G1w2,dz5YjucHrR7q]ǜUXM\ kð~ l^JU! [SCʎN=T9lQ}dq5\V?ݛ6Om/+Y㏯U㳮ZuV]E/?& lZW^u/Rq5HX-o%vgϏ$A$*Digۺm%bzȸnygžyr;\D+  犄M,ܝCn>V3hTo+5El&Kf/d21cҿTJ.G=_𤅬?0_(3!W zyQ 6N46ޒ nb5 *Z<=5*3Vp0|TWN V%oYp`zd~'+سkeT #=<֍ܙ@r9ɠ06u9D~:1x/3b')e)˫rOBTR;zd1=A-=:Zp?Z# P5ʭi:eW<n#|=;ڹ.<87=,>w>pwad8M a ը?1G^{_Mw۴7O{'6>4lj"3<:wRG7'^zw bSorw; \D 8P(UA,k+o-_~Kֱ;}U?o]Q\;yfݩWS?,eߛ2z:𢻙o彑D`DwfTxpWd4x 흰+F|$be;":څ+"{~6Y^TfgB iX&8)R\$BԤ*1nt»}1,g1s(lr6#pmr^d/Q u ]D Q|{ ނ\r< ΰ0;hw{MA+S9{~<3/eJ]H #3ۃ ʩbc Pk GOנhW>ɢdzGq G)mXR}YnAd?͓ mE1Gij]p_ K Rv9:yyvA}݁[iu$7?1 zr|7oO4B/:{e`xCms?bvf+&t#ޕ7ya?l_/E7bsiG?|2u톓eẉ7<!/(0_sݺrofї 47oN5]1ۋisR*/Cph׃Ou?E;žc2wܦӮφ-?xBqd9R4e_ʴ !N^`{$5vH,Y#7̹I6aR G !"*GE3]!Y`+),,0 BCC 9~B JPB3_/qg4{x3]!D MKgTJa<0WdcMutʳ= !& teV`EQznݎfò,|>>ǃAUՒ1cc%BiUM`sD69UBޚ^Ǻ0ve>BqʩeYZH@71_}BqU=8{̳nB ;\e/Bve[-(eg@(;~8w[!Δ*ΘvĴ0#lX,/U|!1pƴ;KB`7ܖGӴi 'x,gJ`Us$`?-cغxK'QV.Z9S!BcZ[hJ~+|Bq&U+Ǫ+ 9WpG jjO[hm\q]#{qmVCCf21coN0-pP'&-jNd{t khVI=R1^Œ=Feyo ! E):x=P*YV`];X!δj>ΘyZ WzIƞliw/?|[$Q,nW TdN槓Eگynr fs`ŗϑ Z}{~ rZʖa@!있+|O oV]GM|pQ74VFPp?;@[KfBq WS6^' IDATgj=Ip 5Y*sD4%<2_TfPW400NB!*M|U2=K+8T;vD4&*} d*MD%_#=&019v>}-)\7~˶d[S6"0PV.;W ~d\z^Gho 6۟-g[Aù[<5RlؚlDguk-7cdڴ\Z3lR aדv6{׳!7OkrXZ:E{29xɝmB<|>{5M]`Ƿї%x WqK3^Wdt\*O~o hc1f*+݂UodgZCn]v}|; lo)D5ø]h3n5e͊wOm {6~}l`W:`4;'I&ov@|^%ïzv }f=jd H@~vۂ٢͜`v#bVgpPS_Fu[#oy!"l -G!6'50PT],:y8 cxw˳v|Xnvӗ(ob^zOclLfM_6?dwio4  Tc:N䆻{gzy[y6+59{ !UT6ڪl+gI׉JNdk.UrNyc?LƠd;^J)$fl vVpî\ā׭Pc 3<6Gy}Y$hwmI+lE}[G݌iB~Fz4P؟]q+ؔx-ig^i+"vaɦyX|~]+~3L:ISg( ͻqiJ]cC8:u3[!$u꽜߲Z#%& %i'vvphڲY r8qtl.V\FRh$< 4ƻy#=&^'3Ϗ-#nÝ#QSM'z$d?nrmlO݌r͝ne+Z=P[9-֭iӀD6H:i43#,-oҙ~I6^F[P+}+*'뜭8vqwO#L&f#2=C:D{ B!D)/Е`WWsҩtg-˓PeD6cНqH^mdeFЬ0x:Dd |iB!*j$o dny+D%? /3FT<79Ȳ@QNdʥO"%B!_I0;j,)hZSŖpD4%>j{_'MZ^(i̚b|[ՇKf[v9lolwӬE9sWPѲLn?alu[VL;sqWXXYί\c6a`}ߦin"cxed£E=L-I#nGTpECL ]"]W1%9sY&X,mj$;X6],lDiHxgQiLrd{ٸr+Shv^;Np|j誟oQPjg>2kd8ېg>{Wpʳ&/>ىNEdpeJߜךFXR2Ig!\f EA-o<Ϡh6?3`MBud(.}{i>|Ŋe9hmէ)?͜5Îo)g'l5t{9Θ!=Xܻ*ْFO]]yt\Yxz1#Zr-Dh(-8G,k2g]ơVF~G+LG>N{G֛y+ @i(1D!)g;-680M͡qQA\~|(෈h֛ǖ B!RmZ<wZӗQphU7$]purjXWt|+HkA+$+ߏ-h;,/y Y@@۽'fpF<߯0NT PmU Fq~:۰ܵ ^tǙS``>|{MV˅LfӛOfEh çph,|~l|#O$xS KN5 ;\۔GB3'-oNv<*a!؃eQ8p9Tk:g1qC3v};طc>ԙjV;h#'׍"Do >#ڛ~/Ká)Ec|>OOd"c+<>_w _oɨdny9;q[*r~a748 so~ζ|]",  p1u텕n>x_+iq'F%t %"m`#X7~&`{S뉷Hڞn$Zwi&}[o"rb^$erp-?zQ̚;2nA98BWs|=}$8{/=WO#`AAzPĞv>$i83?fX4^8;n.=zsՀ)̨J׫[+vCW)0"䶑|dnyy..QN gAӘ~-=OynJ ,:֐`\<2ަ3M <bXA?|SWZp뽍';w.ꨫpK 76bĨHg6X:_cqr9?/~3sǜ9[Ǵ1OpT&=՜8^r 0$S%TMxQAAü\yuM> O=i .`+e*]ǽϾ`fʌ;aΡ>.68̊upv{k7 P=Ut+e=j{ -(\bݨu%2  QԻ'4xgoVד$ EQIH$I|>ʡ_qo=clʛ3ajմ8ŚoScH b""~&O<3ǰT;UnK>177ؽz# T:94̓q÷֍kY> 2wL6C;s 1@J),ുժL ?2mڴiw'RTey rAAo ~_] o/,7_} >Z!~W_Aց$zCF͆{^e_/\'mH;v{&_?2z eb|3xoo20sqGA?߰f*fl6ӆ' p/r>C˲ ]ue΍Wł$ף̈́a(W{@W}̭ l}wSO\GO&]ugxOΚΨ<߷E֭hcg5d_]%"P^ P:Nv.0-/4)`6oGPY=ٷVe ="&&`ڻFYaMHNN&$$D ҈X'ɘT >o/}CǭL9WCs6,&h50P{}g*(fʌ9iǼݿ2i&CwfB ||:j <4ߡ[C*ϺFx~FŌTn 'aD\~|l,ǒ':zn1he(XSAw0\YbbbHIIPYYY:uzUZAoW TCѴ t*S/K~>>Y66(8gJ^Cis3n.;1juCb{yac}ăE4s © (Mǽw'<8x(ccgt'qi;VrdڙOƊp,6؏ù@Οr˰}5-1Mg<~-{ Iߢ?X8DӮCpԩ|ru :z^Eבc?ǏB^Cis.k4W‹{ΌvY e+A7[,JWPPP ,+O ",B\ě3\{"+_^'v뺳5sy-0lW>⍰^?hO3XWf/IK!fԅ \6X#[b*ݽwf)~c͛~r:.{;z1]|'7Ρ]iߘacvjy{Vl&e@j8 lGх93^| xsw==.&giko ~BU 'LZ|t2k_G5%7s3>ЎT{^Mpv;WNd77\>[M;uc@/ߤʱxOU 2m6[\V$ f By Av,óͳ3WB70~QM6SbmiNDƍ#} ϣxJG\+g1r Z^?0mXɯ=Yhh3Y؉ϲ2PCJ]?pOy*}{;pMDҮW{jrWL|N IEyPH8mhE;׆JDzd]d IDAT;IRHm Tcq^w%2@{nh@Kq[dQ k1/ZDxL%- B%olsrr㽿%`A*r-aNN"ݸ\|5|W^Pp(p@ \v;NA&[B]AX~ðk񷬖5+o.G>;o_{Jf]6@o},0\lZp4u%;䇜\ݮ0~'Uauh߶Z8l,9r#Х~̟SqKzޛ1_Ӫ꬟sNN?X nܥ8zkL%ۓ>D+UE$dYF+SNUh>ap*G A*Y9N}ӭbR.?=5O]v/!I2 K2P ѩ˕mj$MdsOqKq }| "JTˌg2f Xޮn-Ep~)y LӈO2EaVT;Cg^^t+.=>^,B*,]j)Uc_FJA( ~ Ja[#DaL&$L& PA{SZa߻ED|^}2MJ,BUU 11/~%$)7ơBJ u.ܾ n'nيT81P4a:G $TvQ)XmfdՉC78HC "VBjn. j*2sP#l.fb$i&<* ך̸1N>/w.jLDHDfm22"-Zn9aG *T['Dhdͅe`Z-<نۉ 6kBh 60N.fԭ1hˌI Aiira2 EQ f۶m$''c20(,%[ìRIIIHd*>&?Ɍ{^uu}Oy ¥J`,0vJcA}*y.Q& $% Y20IF&60*V!/ TݕOfNPhC$࿂0|YBQd9 H޵?~͙]^xK ;d傹,EO^Ĭ%I` 8 rk.݋dd2(J;xYV "66 \*u5؛ʖnu\xpc07 9:ĠfGʘǾu؛u.YVfu>ܜ޽_$Hӻ]*#P52u(/w vp YQD*`s={/SX\V\KWw&2`6)((0 BCCM\\\q{dzoAZeunm5foFӨpq+ J$F+\hUBY!Y,3oc'Pëӳc+gRXcV7A>me'آ;^\Pg2g\YOf|F8uPl۰vڝX'\Aoؓ5L%.-7zyИʹ\z sۻ`zNhb}}+uƿ7NJA4= Zdg43 ؠsӀˋvO dF"PW3JOhj'"r"/O 4>Esӊ]ε'2b<®œ~=  7O n}]UyaZ϶z9A. :lIvJ'U(2(RafYWulMؒqe-SsP_y?1I`>~* OcB@Z2]h8ꆺXT e؊h8ۺ.-86 |( A$jrM;9qUtIA)"CvN\Xƒ}}}v_f#!B')%'5 @&IL, J,wֻ`:PXl Wppl6[X3T _x3WBe+L5Ud7Ns|w@}uRX(ԫ+m:_MN\/y|%== $8tj7$6&u{:nJj<|`괻@FV*3,(W9OZ^|f_E \*./_{  \`IJ ~(Q,U9^_XlƼBzc6O挠yNf JDd0fqXvi)RG$(Q si}f/]ewV |jƘV>c/M|k_yٱn|PQ+M$ d F^F>Gc%O ѵgT,y?ewi>nFk&?סcpDZ{xa}, /{{'T v͈)q WU \ 6t'8T% %$eE+U Il7 pR4FU?At߇Ee8vw2[]xQM>W4}4r(3R)EAYd'w4nEp;^66Dc0ሉ6]d>uuAA{<@طUiUzW/`Hqm;fP ׳t:8ig[V o^Ȓh(ENdK0z٫Y0k[͹v8' W`:;"iubOY-d~{_CsA&}ehL%0+`H4V¤+l|yV8t:=;ƿnՌ}?z*?o:y.rx{Y!7aL49i.pq& GKI3YNg@e B 2c:ś"cV$K$^rj:GTN%7Z7 %O'YX2.p/ 3?v|ȨabBi:`8#Z}Œ2iAG)<=zsT SoygzGڞg>L^9(.0k4) 13LQ1f?F9ntd!uK<7aÒl¤xvL|&<9C qs"Ղ  \F)\PׄILI0-t͢w|kuwsZ>gk5Yheqqݸ5K& @m6Y.nZř6R]rХ>ʝwpW}OD+b eU`P D  I%JitlIq] ANL_X8~Ȝ?t #/'JMo1dLhn.@#Zasn<9+s*  ]o~(d?Uhv#pC`'@,|9/,^!)%lw .M~v..k(}Ȑ*KKԻUNB)0zNs+ /_ tKӶfŒ\}geD(mPiәFOٝm2i4d$.(a"";4;zbؠCH;\AW)hMK+ <(mbMLRhL m E󬒬`*t?mJʲd "*kèr˒?(b}ߢve$ADxݻ`Rd?g]NDbնAA|KHf^KUx=[U[Uݝ>/V>lR]4T(V/a餋 gٻVIAAڶԢSlrFDG4Il&7)|H߾ֆ4QcOh2fnѷfQl]*؛ [Tp9TV&-M]ir9{u î/jYosrZW3UiAAfyMP`Ɂ&75&oʏ&qߙ\r6QglJE ڶ³5 :O Np[Kj^&z Iqg xɴUzgᒡL6mn4֜8_* ,ʆ)b 4X,&b[m 7H{g[W=4M+LUU0 /AA$$Id2~,K`U}u`AAA.pYU@ tpsKIu9r$2k ji`\AAA|;% B`͐Is[6oLg\Dڠ]+\q-[$ozƁ?;8͎}/͂pge[X7熂i%o˘Lt 0(yS'åAಒ$P$ o`:>EpCSqf͞=Ў!=!\䦥l\rP-%"lWcmȁ'vCD~/MSV GP ;~wצAH.u923eݷ_cw䍌l#i)S3q(MS/h8[N[ZW ~d3?Y^D@t#Ep$S+(48뇼p9m͇<3faH-Gqh~bl\H6!({+/ٞB*z\ۂXPYW|-u;pn\$ MNsM|>Cks7VB;0PBn0zƚ _m}d2pI4MOKB=1]AA_0ef&{]y E6mڴ6bRdʄHϒ($RrHQVtLhR> PrnNDGEB Upg矠ɵc43|Z9]oOư).~7]YYj@A^7~f^/ IDAT]7q[φh[S5>BO.>F0YN!8#6Vv& =v܄f/Fq"q6>km0ÛXy&flb(ҁF|v-a>tIe1lECI֬Ω}2?W%Hl_ [mM̞%1pnk$c{4@ٵwXM 'wlI;㞁Miа}٘I*F 7&j>@6QRvlq;^io>ԅݛx`;m0mhJ~_Eky׆Q|-zAs>g{z@8-- IeI,8MX J~AA(;uDh彎v!P]ܐ"dWP¬,;/DKB5$%0Ifh1AڡX*/PBH2Ǻ ‘~.f;Nl$3aL\5J] p瑖ЊAAs>5z} ( .y8lD.ͅv-!!=.I*ڋ?f"#awhdHDQʹܲ ELP-2JyA=J߈%Mӊ{oΘ*RL$t]/΂ UP45R. 2u#9TvNp_0mzN"IaH`UFJ8]S32|N+D;R;F_N I& *]ɎֲD v\@ -M@Mŝe>~E!n MJ컠(xxյ\o>,)n7!HNCjҴdmC:O!!6f5M>}bC0k){](Q2ʐݥ%_1aEzݘ%h?d(G?\3O,ر@#15yFM1wsWܹiV!KV5&đEZDCIlRK}yc_6_k^gĤH&k_+]/ oX'~wl F| j7 ]j((Rв,, "Q5DUqk:jxhLY1)rPc-`וO (W֑M梹w tUErܸ0a#ǥk? A 7nLX<*NMlV}Wd6΁k*,ep;&u~lku;ӺG ͍P-0>401mm3<Tڐ0m/祼{WU0HLL ~{2c?ڇ [U%Aw3;,`'Y v*tTCa+e:ѷ!)) I0L׭LgȶsC۞*вt _v P埙}Lx![`xz\$ŌFB ׅC<*l}ʘ-E ŜhԕH۹e,̩W?}UdkPϱ=fKP(QDcVBKY 䳽`yXBΞGńʟZ(/זOʼn8aX?k l7)sty,1Cu 0t]8#c{ .jv_LM," ,B˿Y`Cn:VWD6b=]]( 5 Qp^N|(#_H߄-QS:?wxLc9ވTp PAs972ocrNgDYi;Dq,Aˆ@ ?EY~<0n{ (q^ўdņfM^殆U8Ny]OyJp4OrwIL=q5=i1?:k:xJC7} Nsw2ZhYR-R?02X3{:,܋=|`- 状+/_[өն)A|0kϠ ]S MS)(pkM9ie\z6=HH Ʉ(Îab \DUAX>[-esDw1@犛bHhU~ޛ<۟g_*~=uZA&'s7 nr3/"He3p}; pۘKGQžu (,Ba.>e鄎Vgd<۫y7bLKxtLE l7\0(lE)  sUZ(ވA( 1ZJfTT97v!m79~7tΪoHs;G\i^ >o>OiCǭL9wQ8?50+}h @O'BKWsoZ :^؁/ K& P 0E6Qxg UՑͦs[1{4 SKq,]q3&]‚) xJs T䃹5ٗnd,M2ڹ6˯ßA",B*H'9KbK7z]鋫R.eF u,Biy|\pom9o{ՙg7y5:!1]QТ`Nhӿ+7RҲ|8ZwD4~2 ܻfӫ,dc#6?rǀqL`{H&07Y~X.b u?q֔N%:'yD59wޑ/X2ʫw\_1y* 60o33?fi\GYލi5/c`aVc+xh r ϟ=}%B/ϠColܒiJ>&[f>ó{0O-zw zK"ZeYVɳ'gB,8 <2u:?7gFcT8ep ~^,vg:i:(*1]SLጫ.bߦJbzqyQ DØ|"Қ*$M1/e|8?\ə oϕG+"Y"œc#hݑDûb2]-C匡K0,ܽAZ92:Z:ЬU.)"BenѩL/ cYLh8Ƶ'*z&S{y|t.riyit}Ug^/^/^_㨍i(.gʬbP|eIaYl ~T3Qz !/~E((|T>{przRzKt*߿7uEs6ۚ F*6 yTi<v fabp`\|t+Ia\%K[Q?ٹʌ>"",1tCӣ~137L`(BԶQԩWTE!.Zm-KW!jJZǏϲg(Š1ʓ|ee-ՙk}m'b:JNZdF?3BQT%, +*YW; #aG*-̺n *Ɍ2⁰I<'USnl؁n1jVK\m0KZQ ͆;oZR|z<쀽/bGJZi9 Lnz,/5㜻ܺ.=wn}fCSlÁ4ә)7eM?UM tqq3ld] ,CC鲣~';%Ұo 4 G(>=q$F,t];TYb$nj$1C q:L|>Q6Βv+ `8oOdxjGvv!JT< ޠ)G؜N]O'r|-ݏϴ]4K('<"H8t-n϶vn?VG`i`D׃Ksmõ; CHD͆ihv@|"( 6VM+O:|Y^Y# meG#7F8H>AKlx{ly)} + !}yAl+ /O^q${s,20LsUon><^&X. #)/zhb[\B?ϴЗN0Du< +sNJ3b8ud]bl6v{YM yXqܔ|`WXĴ]%-Fx8& !C;MqT`rRZ:<8Px'VDn8Mf#-qnB!\Baie5L4v;upZf|*$N (d'&F'^ _P* 8H? O !u0}wʅ[M=lu+?.Kh!X+u[Ca*XÃahR1Z~G|% !CB!P ۊ-f Bm?r#!ߋ`!BQmV~·npthP`>Zʺ !DUHB!5E):|=\1W|6|Y~5!X!BXe!7$EH88p:! B!VjeAWGR1W!jJB!U0Tl8BQ[NH.,Idݾ\eB!,V!jqM?g`~%iGng"V.[ַI"C[%3e2'j !BDt\y*WSTc,I`IZo 4梮MS5B!Bqm7˭ }[qUr\!B!'Z {򼼰d3/, oШÖY/X&mMw!B!VI.[G5u Is^S5,Z۟|: !B!OV K62c6^XVUut)<ϲfeW3yw\{j\epM/B!vը84%2!c7=L\薀CK.dǘ= 7i s7|x CA=d+LUhӥi&)luž3=b0#t6_aObL8 cW|K H)Uy._j9 zԭ !B!8IT;?ߟw:Vޠ K~c7%g/ZU|7:'ά[򳾛ツ&<|ˣ7]™҆]s"B IDATƝӗ`擩Oؗ4k+W!B!-Jjb\[ƣ;W26bc8l+5:=sB*uB's<ܷ/ +2?:|6,}vx8ΚͥLGso@Ѫ7A=SOO@EgGwq 3xs\ް7wv:|g<"x "wB!BqV#O陼:RmNl m;k? osemU;y۵dnxZpzR=QKؙ2Ka: #;XB|ky`VpD&v+B!UU[STBɱ£پ?VSĂ-8;\5 Gz>{2}x:$C$7EgB1%`!=_p9[&~E\_i}rSu.o]݅B!B: ~_'i OyL:$/m?l^SGxi(11[xX[Luq NRAln7*+Bd+xӿU4[d3ԟRz"X!B!xIZ&K2Q0w4{-RT쵑{;Vie$--9C{,8Q>#/'Eq^Ƣ~ B^G4GӬE"76n)a6#=#LDGW4;wUB!QFtRU9U|<۫$_o0_O^?=$Gњ`X/7.?Ľwqg^~]' a)@q͛Vgb||s(E˦ԭg':*v#2x׈NMؽMni |I̛_cƢK㾗ŝ=ʷV qB!ʃ`U/vjCZj^>'RHNР[W.-%]!ԯ J ~9~`>fʧXbøZi`*r Vɺ Tl+-P5lgZYyaa&QI5JhH*ib*j:GuUVv8~e ߽&V= 0!X"m[{!ِԉd:]/#onW. baVB!B4;$0o+nbkL۪FZNв鱗.QoރQ O< ڶnoƗgsgu.s?I XeL ľ̜rN3Ï$ N9[F"ηHO170C*8?Wr99{?+}L54|^ uØ8+)e3۬1V74M o9L [|[.H%~7s0c#^F l ^{@Qc}4LdtC{أw__}i}SHX;/dMVԨ}87 Kf˧oֆ>~j'#K 3yyW{n=X o~̀IgT/B!ɦZ]C՜ }[V]lBV\իy%w⌖z5A =}ڴoS wS P/ۡux0ήxtʭwn"ݚÿ;[iL_EjTw.؈Yaݪt hvj?6KJR)9|5z2h`گV3ڻIۼB6:$;<)HZMqӠ%9]-4ґQ.;rxFZ >RWlצClю[ؘդ#=Gv'Щ} 6=(&m`EoXE[FӉnӓM/*}/dة qߺ92v  4n&0}P=< 4C8@AqeX0U,.f=W/B!IՏqsЎX:WjC;R?];Ԣh7oKפs޲#YqQFȲ~W'N,ln'>.#/ ?#s1"1x8S<VB6>LTg ]+|u7pD+6bG&h9[Ol;IB_[:-4-I@Q ,RfS1|J0-7a 3(>umRo0޼j'qY%WB!BTEWsNi{xu:95q\viE]h#q6W GRmݤ]#֐ھ-vJ:fLyHӈnҌd.|gr|~W'Ցs;Gr< մp7ȭޞ+q}O~3@Ӕ.  ZDVNlkI{Zҩx^|:>QL.Q`(X LnO}5m&#L>x-_ϭl5"8 4abr eݤ-#HۡkRZ]+i7sJ$, >,gе0JZzAcWLE"Urt>ǎf nka\6T#HTibYBEiy[xy[Rmo~!3w=kBQC(fCUUEAUղ׊i%D/ mށ}_HC w÷he!Nj8\ n۰$-urY/(YGQ 1I'bX/b$V<ņ'* yٷ}'~u ݙrMՆ3E`D |pFDSnٱD|:;1 [;j,IlL5%; gpETx~X8yV@j+='#I|+sUKsvauT C="At4rYVi]EPS{"eM6;uJ:044A9j&ڛ)IV,B!j$LЖ%)V~BW߃ڸAfو^"`kݞ.KCk ޟ?BB!dpXθFΨcu:O>Z #.䏮6$Z47ϔB!q'Xqbi8mDWxݛF"𩙏6&O/UW]ڶo@m+ d˗8ЪCchGد w,`[d̻~I^y~,.!B@s4y])\7Y.<;孇Ou\0c}W{\7W^5oLKgL )H{o3.u\a WR.箜顨\Vq/!B9 BE3NڅsxhٝquVLzC4gyɨjVi[hC_KE&O/9G2  [%-$hpʆ;'h(*orbL!d&Sk !Nv9)΂ |6+ 2n'*JIKӿⶋ~t<_]M'wgC/qc4x8{ۃs7:k%cǧPfe} cGQtz9~d3N<)s8t89><g<#ϜUCBKsu8>vB!' B&ۙ kd#{|ݭWaoԏݪzff~e VX?{gJ Y<~/rK*+W݊gIir + 2X4w%$ݓ6XS猧9}-9Hs[t*K2>sne46qm≊iG#WɃO4QOD~g9F\C{&*rfvaGN`ZFR _.gChbufHH򋂘~,XLߢo΋APoo<B!_`!q FpMSϽMVxwaRA>+!Gp}9C]!e$f\ Y i5'˪^eJQ(&xn~lIҝ6ص^EUQP;<+*ru> jXWiO!8o _!B:ׄ8|63M}·:|Yhea[ffk]ױ,6mT<> -_gEs;iFK[ړ7w #HVhUPf|x/g іAIJpjG]O'lߠU:vlb1qDI\8+h&NJ")֎cܸ ^tQRk.\CumWJ_;pҺp9J_+/J#qOGGͮBRSSQ͆(eEAJ>d—pPK }_HC o[q6BH G̎yL$l_6Dv&Ah?NQ4#%RDaUITEֻb^"6ܪUVׇVD,:F0_!B$ !M|ƦL߿ӗ lXSԕ=B!$ !j#Q6kp:Ke pS?B!݁nG>geVfoϥ^@7I!B BZx|0,0nB!IL8QP]R!ByB!B'X!B!IAB!B`!B!' B!B!N 'dhôǣ` QQXòP5؜8G,#՝ R-B!vׄY-bB6)f Щ# 8 KE7(68P] (:dzB!B!N%[_q6Swq׉$#ӎѲq4"_vJ{͝IAqD !B!8 z6ރ?72ٓvh5'E=E݅i`~?zX:bZ$8pDCF4* !B!8 Z،,t~, s-bŜ7\ݺ`6(`]7 ~COn:dQߙC;j`;CljB!B!N AlA"6 2l:O%iS:th;"EQ1 ~Rq{<8#lذWPg/9vЋwGnkXs;~9QHj߇^)ul;W\j؉Ou&Dh'B!BwT ٴ?Y::Dhb?2hvxr=q''2LL ) #):vہ!!ŎK31 -e [ v.|RK(7L+KK^)0-sRqE]g =.*B!BWT4.5"<*k(Xx,LDSU,2LL($nI‹K3Q, FP0gWXك( rd,~3;Kn󦯣tiy ツ&<|ˣ7]™҇?wy^?jYx,AzJ1]ADYV L Sq8l0pE:ihh4EEuDe9\Ѫ7A=SOO@EgGwq 3xs\JyO1.DEk3Oߊ5!K-B!EZ)"IpēSeQ jc|5 KQ~1v L({XEv<8N@ ``& *6l(1(U) 3e0ut@Gvtc.bw(nY>L ӵ]RE-^yC2u!}Giq7vλ^jfwx[jXxdo2#ZV2J)c=ZH(̲aJBBBtmk{Yȉ;~G{ϝJt=lpȱ= >ﵘ}4^GOmZt=_oN6}8%[ _Uװ!2ҡbs#,]B!Bze=0$eӉ$^Bu/*Xި)(/i;"-*!?36+j]v:  !B!v!LaIo.z9,[QKMXByq7cx WSrsRS `69|i9͑M|8.g8M1Tn0pL4(Фc5cy]Pb&a(r-e.'P:=8>p ?0h6a1ʿ552{9K]`8NG^X3>Ȍp>cFN|o粬چȇзQPt}8xp"4,/oy%p~(#]WiZh\9nw9t0B!BOrhD[3 WGԶ8̜Ģ:>h~Y#+T <.4A)s4EZ6* D+PK)*'PVAHSƩ=L[Ş_wKl'Nګ>8'j֠AW{>Aܨ$R2" ;GJr-pUpսljUy01@(ef7B!BstzbN7 R͊ʡzr-oY.?hln6QUt@EU 6Sjvˋ,@(/-`S.cxzUSvË X0|xq;4㢕aono>n1! fgkn&fϱXJ؎F&j 4Vo&hn crxM0vv x((qu>CggY5wu\]c̲_g.Ko~\^8Nezm6ZkB!D̜9eaJ) XZ)i$f.Kw5Z3 v:h,~tK^ڐ`IYb>r,2dR_/'/&uy+"18 4/h@T@庩0, J7R0}Hi'X5 uݠ05"v |ye7X+wEo%ЛxƆ"YX/Ys6Y\!B!~O[Cy1,EnfdE~* ֵ=hXD-e؀>|>8hNڔGɬ$ק)EmhWkНT+B!◦=:y8.xFX S GEE1?1 9 erpjyI.]OaLe'٩2_͋0xs|L^F H$v4KB!Bc=rH460 'MJT3YiPT\H~p'4˘bcvٚP0.-#^L3, 7`:V~TSY\xh vk"X!wSB齱f_y!B!C#F%L>az5Us*LCYU?zMcK,$a_KNjaY(e`z8$2]FA>*21}@KCcXLH \%8\VL|n{ɵnx$_sW\&+B!BdBM"@.թ,#J(*4c%ԇc|iq>%PY׏6<(G\*$7Ч|RYgCa;5xArCTTVMB3ʶ],P\:rzQkį}\(O42LVB֐ *.H3neSu** \r}:A"@H"Xh!2d@)h D-0p (˟pY_'QHg'\{ɟxa4,NJCx߱W!8-c+>gPyy4v^K7_cMefjTz-_3^Z\)?uD|^ !b[6î'@MMK"$L:/gE}F,t]\_j,G;kͬ\-dRTק;|&h?%zˁil8J(.E M 1kS\G]]ܼ$Rsߜ umH*+Ҽx YI 1lv]Ƌê xg?\]yʁ秱m2טI6IY=M3EkK'Wai|?G^O nqI\Ӌ'K$Ie5;j'I8qNb+ OX6aƦ&xn%BqWl冰C|,\RCMm# wZrT6E2|XoW]"l,INnA+WᵹEpVN/LPh'J}& Gp¹7ՄyXn:?= C/Kr_0?|$[c1|Up՟!>H,浻ns5~q]ӌ[ꏩM|qޱ|stj5>?RyUi:Ix\qÓ|Rv>Z>okkO<1| 9Rݩjݯ5櫿^9rCs\Eu5:'Ĩ}qG-_6C!.z\X0q'5gA;"c)VLgϧ92 }K_$fqwpU8,|NκfMzm[J>BIQ|ꛢXQOq0=9Z=,mdF&TϨ*;X@H k1r`,'/Gy[*-%qp]՘i(<%y!v;@!Xn=2ˍ8052AImddK4n"fCߛ췸&Zc8ُhj.>*6_`zdr__+2sƻ1SWrƝOqgz^W~T?{ Mɽ ইweഗ{hd~Ϝxٸ| ,Dn )yգ,;v>>cOߍ篻?ʕWu7j,^|k?? ~x~8%?5We? >μY{HKz/OIBF^=?k?q_x&'3vN굓vdjOn+'c➗W/(;=/nv!\!BuXnEոչ IDAT$ˇ 4 ӓap. _̨AŔL֡a4IDQBE0(ōq|x}^Tz%`yzɯ]R{T]̂!]d5 /HR+Bw巧lG)ME|| )]352j L|ߟM fhxA$ny`:~WMPa\z|#|Pu_e1xzJ- m7` 瞼e0<^|^%BY4-k=INđ(b*e4h'c4NE՚CJhF{X9!B5_S t2Lu5ڧ<FrSheDJGM[A&bF曐7;Wغo$8=ߏ8w2~G{WPh;lY}8o@~\|uW@ Lzm|G/?cF!CO:̞ =9/oʡ\rX;xyxonԇ.ڕ*o3nB[h0)4nt9quq}ϼc}|n^}8Sˇ>Cos `wKN*؍oǸAR C[9~l)O|r"5;&OBnͿ𔻸a<|`yo~S^EeCCvC F U#;?E]:TvaklULN'-Vz/( /by6~j3MCMfIW!nYv{'v}; B(/Bq^0ŹQs^wƴ^4Zb& euysPZī4NIdD4Bs]=M+j74omgUuI2g4A$MUOLsI$ld34<cyrz @az|:=i S1<oݟ=tfSQ; l9n?XHSn|9!{e?`cܙBQjَ+ƧatPɾ1IZ2L{7lySlĿ~G]'<̈́'JA2(85,^zz{S|jZVY8Fzo7;n'3ʡKKC3o/$7^OMd4)Z&y4gL!bv4 e4,TfոNaZ O+H4> "tI4P `(le2 0 I"'TR/PغnCJwwV-nQ-^x`<'[ׇn*(5  7DX0mflIR[kh.2d}OL!ec9c8dUfYA' ~XuI&"D]M;t)IuhӃ׈S%/gPNE=KssAvqupK[,h.FؿӼَE+]CyP2&F~|n|8zٷ,ubr@ū/9(n?װm˲(@ $IZc>[k2-ؑ^hOgPav05(%d1lSh^1ןouJϵ#r¬lsFm|e&oIB#Ne<2E erqQ`xICo C(#{vw{9,b10g.ay m>"\sѱ|q`2w [ 7]r8y[TW7X͍17C+w:NU7u&` m\wԥ\\s\ϡbx_J:j%ϼnbКV'%c9}_c~0p>'Ө5W:r:;N[>}a M!Fu f,&n\BT?l}J6VhP e˧Jh;Pg:N3xD>c¼3 0>l`CGGx:NEqY_FT4QQ[=ε 1j*{s>ꥨ)6Upİ֯nsבU{Sާm z+n&Maވ] 3gyi7n_Bt%O⺝}/Ά8j 22HYI~ K%|2?IME%&VX>:?t͜z=WRFY 99Uٺݢ7nG5ǜu(9x =َZ}ݖ6h7,"/C!b(jA,?3,;;խF) T5嫁C7l$ umv[Yx̙\r=]b-̈́\>dƘA0׏'[ص`y,҉iA 7?MKn/Lu\]c̲_g.Ko~\^8Nezm۩nÇoPoxl w%O3O^ƫ,}K~ \zH d!螙3g², H%yZrL}UfWt+@TkuZ`3˶_6)# :PeT{sXt +,fbff<@n HaJʫ(UEyAX;oay舆'@Q֩N Y+O}4,|)ʖD  ~CQq+|[otNUJKOdG ~׃~ƸO'"BYذw8,z(]Dj..Ǡtoxq7]+`@*!B!fgm9m7in iv/B!6+z"]!B!b B!B_ B!B"H,B!A`!B! !B!EX!B!/B!B!~$B!B B!B_]mD"B!Bl8B!B RjܗB!Nu!~$b=Deՙ}H(BN!P!:O`!6QIe'BlR %PJG$+B|D=]%//BlD]hbB!~^b&TB!ϓBH,DveB_u˛bs%Нes,Bѳz BtBl ]XB!_wsi%!]Н@+e:*'BT  ؄:[iJ&BlޤNXҀ>ґP hh Đ7`SJWX2!Blz Q`Wk6QMmLNh\g@_QPT**rMr?=m7&3mY~#c dE &~͏aMߟu?s'Tڡ>ϖy"8h4e]xR+&ϯ)cخ"gՏ _ɾ⤧%CLKIH___lAju^88k!Bta>nGͨe\!6 ;.4&`vfVÊ. & S_ ^Eiadɖ%~?{^_9e-\(9?qLl~|}<|Dkv ƾ\n-n;9407㷡r ~n|]ڙ};,θUf[ G|ŋv"eOs_m=g#|X g8ku&] u}[W7z}G!Vf]~Yr]φط?w$@oLqۨYmMMeЎNXi\fXn2 ϫ6Xܖɏp_R|ݼq6@r9r~w{|g H* <>/?~O߁C]{<7 եfg/.Vgsr\|$O/ߍ =y%'o?ŋG{pQ:YYe,B+ hf !̬|aQK8:5=` R.Dmؚp2<755s]{إ ߛڮg佧cSy7/w1L='^ A@Q;璘9s=I-k,iӓY^ Oj!;qck1/|̀o>2eyG1z"o`ϙ2hΛ|1Eży}Z3[[X]=/}#m8f u ϣ*cN㰗_rŘ]0ikwyG8Ӂկ,~nщ_ωK)nO/?_]>ߟKLjk3V27pW|.o?OϾ4뺫m`k !V>:ۏԝi,^\$5SV837ɜ`(0@gX2' 8tɨb2 ʹZ'%`Cc"D`jĐqXpۮ&1+ѺLk!covFO,Jvݕ6s_nqk8 t"4u6ɕ8{47s7sYB Yh =Uw@3>|W>-#\TSlG[{yYkrY. O? -ϣ~U3*2'y_ ,g3F;D_u$+Bo:]c!zFfcbFÇ b}|5-{6 y5+Z,kJ$บg*vDKǛ"yfFO?_}p=1 ܌z_RZ'~.<}ɻ2(ʀk*8ܗϤ?E[ _SNуOmjxgp>r1KǏbSW{g>-7Z㪋8܀rK rh*[j^띊3?oKx)7]KW ۜ]Cmy(Rǜ*/>^cg3ɼ8dz{|~:L,qr!Bl:9]cœzOS͚^MÜ0P$4qG㸤BѤNjrL}0y%ipH_$SC?(QǤӾ01QvXk<58Isɣo~F8fwi;p//BC$㯠_^iK)^g;1x-0Mg~ O+M.&n[Y:mP%051{`A ȍxy3.?; P3U̝inZ2B!Ħaƪ:9]gușͦۗ錎K,X\LY鲴%ᤚ=(VT\>%"1[㸩~aA`Ds?hu]רy= yldh#*8 kG:xOَynb ͞[y (XfP\']o??-؊?߲?}#ǣ6Z:5&Xu{gKnwv^߮B!楻swB ] 3\4jFk[oG?52iQ:uP =bxCTOvXVDmv'K, RnɫAOg-!LY&M̝WX]\ s;?r8 |aPԷmſ\f|FxW5Ӻyiѹ1aߞ[]1N9\e¢3:մzՅ,_j.`wwnC)^F|  M -5`~]F)f~3˷"$B!9>◬m(S}ҡ6& -PLYW#u:U8 GQ-+rvY7g6Q9j«\WCelefy>cڗMwݞ+#;_ :coϳlW~˯f*%?Ù>eTWR&݌ϯٶT*̪Xd$1!b3hlll3sGuuB `WÒ0hGMfijn|e(8ʌ81N:wf2+',nL]0s8wطYrO0<~ ObvyWlVQ˾7/7pK6M y O͏c QЩC⏇pPLϥ?Ű W3ʻ\k'l WٙqG $vGA.7glG95|mH\vJ=4-O5, gNoG}3+ٖq]qZzN/s]#2mүү/ﮕ+WRZZcB>mXF;7":?rc<=L0."7<-~u#8Cxǹmѽo!v)\hto9G\\|ߍ c%K&]נ@] IDAT*yq8p5ʨk[f{98i8ƀYZ5W6uZ=if|N!u Vk+vuOsԹLhmb<;#.$-Sap@MIIƘE,F$[r32NIJ"/4]'obh?gN#sg^>r==&"4mz1|={{hH=k<Ԏ.MԂϧodW2L}D\D 4Pap븸x -I@9Vem+&hnl!ɥ ׋n{e:^8fBy~Mޙ`nmA_uY߾\׵/~򦳶Yeř粮 nm$,;:#[ffe3ץZ~tyvԔ=Y&Bt;:ܺ:9awQf 6qtЫZ`*u̬ItSe'{`u$61#8{#zl\8Ec#ZWv]: t͟;p_3gڙlA{(8 !+֖Y(۾̖l>mj;:upC4Wu?Znsƍ&8]co* ִ遱ϤZdS7 He[h 9ɎϘod̆;lOWPMauY6g ӿvͪge:i-;:Bیl&܎lpe94 nXO!iTbzsq\;S;.zuhTARժQ-L@Tդ#s26x/oftlϙef8 >[Qz}a BיAszNy]G}t%+أ\4 'FpWs-Mzm*Ek5 B7dW, n}`7-3m0 Yte.>,M_̊}7}N1+ᮒ&B!zˎf[Kׯ-vlAw62 l%0+E>H*` xU\Zin5 v]zNg] Ӡ_EM%@ɳ:ڰ룧8g Z;slr7wof 9"tfXDg;f|Lp>OB!D{ɐϨYt>g wwGv=J,D\tP z̪ۖMP3p<9 5ۛR#T75ئ֩P7bl 7R@83Mgk3@;ifڵ]ibeeM!4'(9QG}|7nBlX RBKpا^>E>O b5jml*( Xl[YQBlܶ_ ~t8$:o4z" BZ[lA2ٖg۶}d: vZ&X`P )ZpmSfn^sXph*u ցV ~A?.5wq-_jm83v /xF&B!6@-\ Vau۾Lsa],ئD4bSNj^ձDZrpu 6۔{)|,ɲHR*gvVFSSǔXuu}*Q>Ye͛3dfӁnFn<-eev;jvwG휻r*/A)=4 2:j\>YC/?9OQG8xp U̩w8u*4#$IPׇe(T.:4 Mf3YfGљn~M3@ hݑ˄B>5txmnGrL+2Bh*Cvm1!β1xwRUSe1F$,\snj2f^"*_iSkM/]W=aƨ^96y}^gгX B=Dsc,7.QgT@Jj+FTQ_F/Ŀη/I⥬JR;2>\x.@V26܊? R7UQ`. V.^N4TIt?^X~\RGS~[h?-yTՙwꍦekAU DG=QPxQ'j :dq̌kϫ&dhp .(  h\dYz.Guu߾}Vu5(Ut )>&;9CRAlI6W6|di^eijW$98';gA:NpPW9OCuFh0+A+t)ׇQQ 4~xY H_un5(W{?E\|<횣NJ|I7K M˿\2:s *Wy^43ܻR#qH}"{߭v\=wMk-(뢻g"tiӮW-RQZ>ye1O|Dy2,/g3kpVd>C_%Vl*iPZspuF~^f;˗oaճG[Gju6H  (vDĩWv^HV&yBij =H9 5Y$Rmi 0"ȝ'< FлXcwiś#R~L;j,Wss]Ż/n˻Ci~V j'tihJkY>ns$J@md[&9۴5Wz˴նmii lw/*O302ƶc[ {غK.vݝofG3Vn;j=~m^jDڒ/{\6X&z1M59:ER;İdu-]u/7-Tڮp,V2Lö#xXcÔx*e x(1ESQ\α&kƳ(j5HCA:J:Aغu`/T68|\U 9P p͸XCF1Mj&Con0EA.* UaTGl*[ӜM`uajhqMYĴУYL*y` +ul Y({+]zC5XylwO@~w]@5%bkt1k,_d_ 97qs`هf݀8gabb8?3!r#yv5Wj1n=7TOq|ΖZQ\?Rvu~ T;HN._ރ`ۡӻ9Lع#Moh?>ǞitwZؽr@aol(z>LkV`< ϛvU=%}ߴO,| D,.C}Nd8u.}a tW]{ YGK_bJ"^6_ʔ2c|E}v }&keT f /{'?$bk<WBf ZaԢ>L*V**]'\XSG1dNvڟ*mRhӿ[Ɔ*@\݋S%I|T x=LJΰ_ofqNyqA,sAM )O77YO47S1=9M{Q+FqRaUƑX&U e`ƣ V|$H ,6O0ٓ#TѿkΊzt,0ia) 4|O7xprW6>wNE,`xn:~J1z'_{7/<߭ggmֱp7v3JK1>48k9 ;V.7+?as荍e5~3m*`\,zGJnƂm6}{n?eڱL)@-MPP4j2RdSky 7ji3_Yut3LcKr yr;7lb nCJQŻ6m3,|=1BG5< f~:"U0s(+<'7yc kYSk)gbuivJ©zT Z@n%kbҢ05IՁ&f̾k4يWxq a*ŊI<5ZUllď戲->G)2XA~ӋS soyN:߯κ\ \ǂЖ `UU^E'MX#f:%H8f3L-o#«`4(#W ͈X$BL'R;JSn`(7dmTUM@/E ;X獧n`(1+mѳ*<~:6{;GS ;2 Cڼq`ʂ;y3NXJ S9G! y:ЈSXz.g۸JetPuVg襃?\Uɛ~}Άײ5N:iQiAAFMl86-b|K*΂!yD6&߇_9\2e.L1# 2ֳmGA005QݛgUH 50xxL zp)ݲ#dwư0N.o J"=!^ݑV6 zSjgr"L ]iNһ]Ȕcy-l$hIV6O"콶CIAŏ@L%n^7@Nĩ|`:(S:F=ҭ룬]}ul֠.j5,Z"5[v%P?C0 Q)S)E&е+ePc^Z5#M4"YjP]Y25kyj^CjRfa A)`mc+fR;{ߛl)+rE-}rjF"`Gk $QoY?ܳuLV ÚR=Fuu Vr=v h2+!DjAv|,'FM&S*aGѲ{4ZQ%6]Lgr~~?+T@SGqGvgƈnX6Xv"ZA]!9=Z K(`cr4Ekᢿ1 zz!j^Z˶{YBAqz;jRS݀ZXDwFQIkGM6)@H]]j% aL@"u4(m?`qzƭ<- ^6lO=zۈa)#K_˩r6k1EU R:b?'S./ņpܑ`s,kBʯKYr!?2=īn,+~Ok>P Sn~B)¥B;m+)s5 M]Lmҟ[,63oMRUUEEA4Y\ 8XIA:JHM4t~:~Ahq)ǢG2'3sZ~|&vh1O~~g24jQ %l&̔!0x:)òMٰm0yQ.iC_dHɡfe9-S:lC^{cr8>cy)Ϡ |՗cs9glrs(ч ڕg?Y)2&TuO< ZͶ1z,mXʝl%{?%Ī#|{- z1f%|ZVǕCX՛x&_2űTGgc5(U<ċa[6f4BIJ1ƣ([V=#5gO'뤾\QGL &xl:>a!#aE)\=uc+639ƝXqgXУ? ]u]y" %L2aY\953 j2mQk+[j08ʶ7r?WkW5&}L~}*nI6>p^{Ҋ‡P€Y,XAD>Tq:1_A9.&їeKyW F̠x?컏SG]%{*vZI\5+~W4 +S%JeUPB(\~]Nwkt%^ׅ<OSq5*spr UmR\ v*rô<Yv1Sf]PE 2Vڅާ_۞7Kt*&<_˥wn Dv~.Ӽpoo&o ΢[ U9Ω}b> k:`9,>vE#gr9Y؃(NiWqI~>ΞʀGWӟBSC ҍc3‹2FQl>\>oetyz >U0sY+y̸b:*?Rt}ћsx_ИwUV~q)y/ fi>WOBS.Gru7C Cл3c *0h~5L+(M.cŚm473ot ϟBhC-V>]`İ`Y6FZ#HiQ00e.3\O0ؼ?M:3HIqbnaխm3FKL ":jk=w،R[A)(8k6Qt,"fEn/*"k;}^|= es^ ðCZ1vsEoیC\}N5R`,,(8_{ƚn08㩯o/H5hцzj%lut (.*Q[kXumYW#7px޷eYfں0V0DiOmL1M)vC;Jmc@A!3$\@OiJ0Q4a_`bXaXhbD ӻw L:ALDv$ud"گ>|SՉoD'nÚMIDAT:M Lإ|B6ydsime2كtoIk= XA(~#鄯&<ߎ29LA(~=H4e]mӯMnGܶX:ap18Jֿ]6s ?mv鄯(y 1D B 5%e^&e"lXګO1z\1r,% t^6D~o1SG,,iH%PS(O'S]u#yd!  $ÏzhpT7]d:g?} `A$FIJ;Ouu4Nq% d4T7Y}:lS!Q:l΅M%uɢΨotQd_vA+")Ђ BHREr߈p6x d`Adr)Bi*v^ {WN$, d%YYh;'s)~A"'~S;6:Y7Y;J ;tl$AA,̟MUGCv7E( D BxEmnMv%~p{- doa}v$ dA,m$8"+lu \s+IANtҭQ7Y~!5"d*DW6U$k_lSA#d"hd7YTb6[qD Bt$ *m9پ "ng_D{xʽhA3H拒lL#Fԑv%lR`ERSK{à3S- t٤B'3 gslo.D ؒ("Րݶ~Ife,e˾}ӧOA_>^3^tht^Y iyv{OUEQ$ZAT2Is%~sNVD BJQΤ=x/fWOw.H $aDA8+X>h]+(s.,K'3t8d k2d,w].ӟ #XrHҡ^:w&b]A +O&ԜF%YN D BV,,q=UtT ׆Mj~ȶ_A2G t 厼iN7~-AFLVSӟH t"QU5+ g1f+שery- t&ٮm _} ="QU5er2'N'A&~3KdXA8de_,ǁ ΝQ1e/TU܂ ·FE>sqAG '2~8NuLAvr=G8+pqnkLۤr~q*[AA8QHĶ#"66"~r\mۘeY-94DJh&_H!m>[~Ҧs{ Ǔa\nA:F `41M00M3]⇷F4t]G44Ma q-bs!ji5dH$XA8:V 8t,X,ao !UUEu,#=WJMgoY} 2J,O]r.ch4kƲ,(a  9WNk.vhAD&ש&:jIENDB`spectral/CMakeLists.txt0000644000175000000620000001776313566674120015140 0ustar dilingerstaffCMAKE_MINIMUM_REQUIRED(VERSION 3.1) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) set(IDENTIFIER "org.eu.encom.spectral") set(COPYRIGHT "Copyright © 2018-2019 bhat@encom.eu.org") project(spectral VERSION 0.0.0 LANGUAGES CXX) if(UNIX AND NOT APPLE) set(LINUX 1) endif(UNIX AND NOT APPLE) include(CheckCXXCompilerFlag) if (NOT WIN32) include(GNUInstallDirs) include(cmake/ECMInstallIcons.cmake) endif(NOT WIN32) # Find includes in corresponding build directories set(CMAKE_INCLUDE_CURRENT_DIR ON) # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) # Set a default build type if none was specified if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to 'Debug' as none was specified") set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build" FORCE) # Set the possible values of build type for cmake-gui set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() set(CMAKE_CXX_STANDARD 17) # Setup command line parameters for the compiler and linker foreach (FLAG "" all pedantic extra no-unused-parameter) CHECK_CXX_COMPILER_FLAG("-W${FLAG}" WARN_${FLAG}_SUPPORTED) if ( WARN_${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "(^| )-W?${FLAG}($| )") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W${FLAG}") endif () endforeach () # Find the libraries find_package(Qt5 5.12 REQUIRED Widgets Network Quick Qml Gui Svg Multimedia QuickControls2) if(LINUX) find_package(Qt5DBus REQUIRED) endif(LINUX) if (APPLE) find_package(Qt5MacExtras REQUIRED) endif(APPLE) # Qt5_Prefix is only used to show Qt path in message() # Qt5_BinDir is where all the binary tools for Qt are if (QT_QMAKE_EXECUTABLE) get_filename_component(Qt5_BinDir "${QT_QMAKE_EXECUTABLE}" DIRECTORY) get_filename_component(Qt5_Prefix "${Qt5_BinDir}/.." ABSOLUTE) else() get_filename_component(Qt5_BinDir "${Qt5_DIR}/../../../bin" ABSOLUTE) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) endif() # prevent error "You must build your code with position independent code if Qt was built with.. if (Qt5_POSITION_INDEPENDENT_CODE) SET(CMAKE_POSITION_INDEPENDENT_CODE ON) endif() set(QML_IMPORT_PATH ${CMAKE_SOURCE_DIR}/qml ${CMAKE_SOURCE_DIR}/imports CACHE string "" FORCE) if(WIN32) enable_language(RC) include(CMakeDetermineRCCompiler) endif() if ((NOT DEFINED USE_INTREE_LIBQMC OR USE_INTREE_LIBQMC) AND EXISTS ${PROJECT_SOURCE_DIR}/include/libQuotient/lib/util.h) add_subdirectory(include/libQuotient EXCLUDE_FROM_ALL) include_directories(include/libQuotient) if (NOT DEFINED USE_INTREE_LIBQMC) set (USE_INTREE_LIBQMC 1) endif () endif () if (NOT USE_INTREE_LIBQMC) find_package(Quotient 0.6 REQUIRED) if (NOT Quotient_FOUND) message( WARNING "libQuotient not found; configuration will most likely fail.") endif () endif () find_package(Qt5Keychain REQUIRED) find_package(cmark REQUIRED) add_subdirectory(include/SortFilterProxyModel EXCLUDE_FROM_ALL) message( STATUS ) message( STATUS "=============================================================================" ) message( STATUS " Spectral Build Information" ) message( STATUS "=============================================================================" ) if (CMAKE_BUILD_TYPE) message( STATUS "Build type: ${CMAKE_BUILD_TYPE}") endif(CMAKE_BUILD_TYPE) message( STATUS "Spectral install prefix: ${CMAKE_INSTALL_PREFIX}" ) # Get Git info if possible find_package(Git) if(GIT_FOUND) execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse -q HEAD WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE GIT_SHA1 OUTPUT_STRIP_TRAILING_WHITESPACE) message( STATUS "Git SHA1: ${GIT_SHA1}") endif() message( STATUS "Using compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) message( STATUS "Using Qt ${Qt5_VERSION} at ${Qt5_Prefix}" ) if (USE_INTREE_LIBQMC) message( STATUS "Using in-tree libQuotient") if (GIT_FOUND) execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse -q HEAD WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/libQuotient OUTPUT_VARIABLE LIB_GIT_SHA1 OUTPUT_STRIP_TRAILING_WHITESPACE) message( STATUS " Library git SHA1: ${LIB_GIT_SHA1}") endif (GIT_FOUND) else () message( STATUS "Using libQuotient ${Quotient_VERSION} at ${Quotient_DIR}") endif () message( STATUS "=============================================================================" ) message( STATUS ) # Set up source files set(spectral_SRCS src/notifications/manager.h src/accountlistmodel.h src/controller.h src/emojimodel.h src/imageclipboard.h src/matriximageprovider.h src/messageeventmodel.h src/roomlistmodel.h src/spectralroom.h src/spectraluser.h src/trayicon.h src/userlistmodel.h src/utils.h src/accountlistmodel.cpp src/controller.cpp src/emojimodel.cpp src/imageclipboard.cpp src/matriximageprovider.cpp src/messageeventmodel.cpp src/roomlistmodel.cpp src/spectralroom.cpp src/spectraluser.cpp src/trayicon.cpp src/userlistmodel.cpp src/utils.cpp src/main.cpp ) if (APPLE) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework Foundation -framework Cocoa") set(spectral_SRCS ${spectral_SRCS} src/notifications/managermac.mm) elseif (WIN32) set(spectral_SRCS ${spectral_SRCS} src/notifications/managerwin.cpp src/notifications/wintoastlib.cpp) else () set(spectral_SRCS ${spectral_SRCS} src/notifications/managerlinux.cpp) endif () set(spectral_QRC res.qrc ) QT5_ADD_RESOURCES(spectral_QRC_SRC ${spectral_QRC}) set_property(SOURCE qrc_resources.cpp PROPERTY SKIP_AUTOMOC ON) if(WIN32) set(spectral_WINRC spectral_win32.rc) set_property(SOURCE spectral_win32.rc APPEND PROPERTY OBJECT_DEPENDS ${PROJECT_SOURCE_DIR}/icons/icon.ico ) endif() if(APPLE) set(MACOSX_BUNDLE_GUI_IDENTIFIER ${IDENTIFIER}) set(MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME}) set(MACOSX_BUNDLE_COPYRIGHT ${COPYRIGHT}) set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${spectral_VERSION}) set(MACOSX_BUNDLE_BUNDLE_VERSION ${spectral_VERSION}) set(ICON_NAME "icon.icns") set(${PROJECT_NAME}_MAC_ICON "${PROJECT_SOURCE_DIR}/icons/${ICON_NAME}") set(MACOSX_BUNDLE_ICON_FILE ${ICON_NAME}) set_property(SOURCE "${${PROJECT_NAME}_MAC_ICON}" PROPERTY MACOSX_PACKAGE_LOCATION Resources) endif(APPLE) # Windows, this is a GUI executable; OSX, make a bundle add_executable(${PROJECT_NAME} WIN32 MACOSX_BUNDLE ${spectral_SRCS} ${spectral_QRC_SRC} $ ${spectral_WINRC} ${${PROJECT_NAME}_MAC_ICON}) target_link_libraries(${PROJECT_NAME} Qt5::Widgets Qt5::Quick Qt5::Qml Qt5::Gui Qt5::Network Qt5::Svg Qt5::QuickControls2 Quotient cmark::cmark ${QTKEYCHAIN_LIBRARIES} ) target_compile_definitions(${PROJECT_NAME} PRIVATE GIT_SHA1="${GIT_SHA1}" LIB_GIT_SHA1="${LIB_GIT_SHA1}") if (APPLE) target_link_libraries(${PROJECT_NAME} Qt5::MacExtras) elseif(LINUX) target_link_libraries(${PROJECT_NAME} Qt5::DBus) endif() # macOS specific config for bundling set_property(TARGET ${PROJECT_NAME} PROPERTY MACOSX_BUNDLE_INFO_PLIST "${PROJECT_SOURCE_DIR}/macOS/Info.plist.in") # Installation if (NOT CMAKE_INSTALL_BINDIR) set(CMAKE_INSTALL_BINDIR ".") endif() install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR}) if(LINUX) install(FILES linux/${IDENTIFIER}.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications ) install(FILES linux/${IDENTIFIER}.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo ) file(GLOB spectral_icons icons/hicolor/*-apps-spectral.png) ecm_install_icons(ICONS ${spectral_icons} DESTINATION ${CMAKE_INSTALL_DATADIR}/icons ) endif(LINUX) spectral/.gitlab-ci.yml0000644000175000000620000000653213566674120015024 0ustar dilingerstaffstages: - build - deploy variables: GIT_SUBMODULE_STRATEGY: recursive build-appimage: image: registry.gitlab.com/b0/qt-docker stage: build script: - git clone https://gitlab.matrix.org/matrix-org/olm.git && cd olm - cmake . -Bbuild -LA -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install -DBUILD_SHARED_LIBS=NO - cmake --build build --target install --parallel $(nproc) - cd .. - git clone https://github.com/frankosterfeld/qtkeychain.git && cd qtkeychain - cmake . -Bbuild -LA -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install -DQTKEYCHAIN_STATIC=ON -DBUILD_TRANSLATIONS=NO - cmake --build build --parallel $(nproc) - sudo cmake --build build --target install - cd .. - git clone https://github.com/commonmark/cmark.git && cd cmark - cmake . -Bbuild -LA -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install -DCMARK_SHARED=ON -DCMARK_STATIC=ON -DCMARK_TESTS=OFF - cmake --build build --target install --parallel $(nproc) - cd .. - cmake . -Bbuild -LA -DUSE_INTREE_LIBQMC=1 -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install/usr -DOlm_DIR="olm/install/lib/cmake/Olm" -DQt5Keychain_DIR="qtkeychain/install/lib/x86_64-linux-gnu/cmake/Qt5Keychain" -DCMARK_LIBRARY="$PWD/cmark/install/lib/libcmark.a" -DCMARK_INCLUDE_DIR="$PWD/cmark/install/include" - cmake --build build --target install --parallel $(nproc) - cp install/usr/share/icons/hicolor/256x256/apps/spectral.png install/org.eu.encom.spectral.png - linuxdeployqt install/usr/share/applications/org.eu.encom.spectral.desktop -appimage -qmldir=qml -qmldir=imports - mv *.AppImage spectral.AppImage artifacts: paths: - spectral.AppImage build-flatpak: image: registry.gitlab.com/b0/flatpak-kde-docker stage: build script: - cd flatpak - flatpak-builder --force-clean --repo=repo build-dir org.eu.encom.spectral.yaml - flatpak build-bundle repo spectral.flatpak org.eu.encom.spectral - cd ../ - mv flatpak/spectral.flatpak ./spectral.flatpak artifacts: paths: - spectral.flatpak build-osx: stage: build tags: - osx script: - brew install cmark - rm -rf olm - git clone https://gitlab.matrix.org/matrix-org/olm.git - pushd olm - cmake . -Bbuild -LA -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install -DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt/5.13.1/ -DBUILD_SHARED_LIBS=NO - cmake --build build --target install --parallel $(sysctl -n hw.ncpu) - popd - rm -rf qtkeychain - git clone https://github.com/frankosterfeld/qtkeychain.git - pushd qtkeychain - cmake . -Bbuild -LA -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install -DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt/5.13.1/ -DQTKEYCHAIN_STATIC=ON - cmake --build build --target install --parallel $(sysctl -n hw.ncpu) - popd - cmake . -Bbuild -LA -DUSE_INTREE_LIBQMC=1 -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt/5.13.1/ -DOlm_DIR="olm/install/lib/cmake/Olm" -DQt5Keychain_DIR="qtkeychain/install/lib/cmake/Qt5Keychain" - cmake --build build --target all --parallel $(sysctl -n hw.ncpu) - /usr/local/Cellar/qt/5.13.1/bin/macdeployqt build/spectral.app -dmg -qmldir=qml -qmldir=imports - mv build/spectral.dmg ./spectral.dmg artifacts: paths: - spectral.dmg spectral/macOS/0002755000175000000620000000000013566674120013366 5ustar dilingerstaffspectral/macOS/Info.plist.in0000644000175000000620000000222413566674120015741 0ustar dilingerstaff CFBundleDevelopmentRegion English CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} CFBundleIconFile ${MACOSX_BUNDLE_ICON_FILE} CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${MACOSX_BUNDLE_BUNDLE_NAME} CFBundlePackageType APPL CFBundleShortVersionString ${MACOSX_BUNDLE_SHORT_VERSION_STRING} CFBundleVersion ${MACOSX_BUNDLE_BUNDLE_VERSION} CSResourcesFileMapped NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} NSPrincipalClass NSApplication NSHighResolutionCapable True NSUserNotificationAlertStyle alert spectral/qtquickcontrols2.conf0000644000175000000620000000043213566674120016557 0ustar dilingerstaff; This file can be edited to change the style of the application ; Read "Qt Quick Controls 2 Configuration File" for details: ; http://doc.qt.io/qt-5/qtquickcontrols2-configuration.html [Controls] Style=Material [Material] Theme=Light Variant=Dense Primary=#344955 Accent=#4286F5 spectral/qml/0002755000175000000620000000000013566674120013155 5ustar dilingerstaffspectral/qml/main.qml0000644000175000000620000001055013566674120014613 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import Qt.labs.settings 1.1 import Qt.labs.platform 1.1 as Platform import Spectral.Panel 2.0 import Spectral.Component 2.0 import Spectral.Dialog 2.0 import Spectral.Effect 2.0 import Spectral 0.1 import Spectral.Setting 0.1 ApplicationWindow { readonly property bool inPortrait: window.width < 640 Material.theme: MPalette.theme Material.background: MPalette.background width: 960 height: 640 minimumWidth: 480 minimumHeight: 360 id: window visible: true title: qsTr("Spectral") font.family: MSettings.fontFamily background: Rectangle { color: MSettings.darkTheme ? "#303030" : "#FFFFFF" } TrayIcon { id: trayIcon visible: MSettings.showTray iconSource: ":/assets/img/icon.png" isOnline: spectralController.isOnline onShowWindow: window.showWindow() } Platform.MenuBar { id: menuBar Platform.Menu { id: fileMenu title: "File" Platform.MenuItem { text: "Preferences" shortcut: StandardKey.Preferences role: Platform.MenuItem.PreferencesRole onTriggered: accountDetailDialog.createObject(window).open() } Platform.MenuItem { text: "Quit" shortcut: StandardKey.Quit role: Platform.MenuItem.QuitRole onTriggered: Qt.quit() } } } Controller { id: spectralController quitOnLastWindowClosed: !MSettings.showTray onErrorOccured: errorControl.show(error + ": " + detail, 3000) } NotificationsManager { id: notificationsManager onNotificationClicked: { roomListForm.enteredRoom = spectralController.connection.room(roomId) roomForm.goToEvent(eventId) showWindow() } } Shortcut { sequence: "Ctrl+Q" context: Qt.ApplicationShortcut onActivated: Qt.quit() } ToolTip { id: busyIndicator parent: ApplicationWindow.overlay visible: spectralController.busy text: "Loading, please wait" font.pixelSize: 14 } ToolTip { id: errorControl parent: ApplicationWindow.overlay font.pixelSize: 14 } Component { id: accountDetailDialog AccountDetailDialog {} } Component { id: loginDialog LoginDialog {} } Component { id: joinRoomDialog JoinRoomDialog {} } Component { id: createRoomDialog CreateRoomDialog {} } Component { id: fontFamilyDialog FontFamilyDialog {} } Drawer { width: Math.min((inPortrait ? 0.67 : 0.3) * window.width, 360) height: window.height modal: inPortrait interactive: inPortrait position: inPortrait ? 0 : 1 visible: !inPortrait id: roomListDrawer RoomListPanel { anchors.fill: parent id: roomListForm clip: true connection: spectralController.connection onLeaveRoom: roomForm.saveViewport() } } RoomPanel { anchors.fill: parent anchors.leftMargin: !inPortrait ? roomListDrawer.width : undefined anchors.rightMargin: !inPortrait && roomDrawer.visible ? roomDrawer.width : undefined id: roomForm clip: true currentRoom: roomListForm.enteredRoom } RoomDrawer { width: Math.min((inPortrait ? 0.67 : 0.3) * window.width, 360) height: window.height modal: inPortrait interactive: inPortrait edge: Qt.RightEdge id: roomDrawer room: roomListForm.enteredRoom } Binding { target: imageProvider property: "connection" value: spectralController.connection } function showWindow() { window.show() window.raise() window.requestActivate() } function hideWindow() { window.hide() } Component.onCompleted: { spectralController.initiated.connect(function() { if (spectralController.accountCount == 0) loginDialog.createObject(window).open() }) } } spectral/LICENSE0000644000175000000620000010437113566674120013375 0ustar dilingerstaff GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. spectral Copyright (C) 2018 Black Hat This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: spectral Copyright (C) 2018 Black Hat This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . spectral/.gitmodules0000644000175000000620000000037613566674120014545 0ustar dilingerstaff[submodule "include/libQuotient"] path = include/libQuotient url = https://github.com/quotient-im/libQuotient.git [submodule "include/SortFilterProxyModel"] path = include/SortFilterProxyModel url = https://github.com/oKcerG/SortFilterProxyModel.git spectral/imports/0002755000175000000620000000000013566674120014061 5ustar dilingerstaffspectral/imports/Spectral/0002755000175000000620000000000013566674120015636 5ustar dilingerstaffspectral/imports/Spectral/Effect/0002755000175000000620000000000013566674120017032 5ustar dilingerstaffspectral/imports/Spectral/Effect/CircleMask.qml0000644000175000000620000000067113566674120021564 0ustar dilingerstaffimport QtQuick 2.12 import QtGraphicalEffects 1.0 Item { id: item property alias source: mask.source Rectangle { id: circleMask width: parent.width height: parent.height smooth: true visible: false radius: Math.max(width/2, height/2) } OpacityMask { id: mask width: parent.width height: parent.height maskSource: circleMask } } spectral/imports/Spectral/Effect/RippleEffect.qml0000644000175000000620000001433013566674120022114 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtGraphicalEffects 1.0 import Spectral.Component 2.0 import Spectral.Setting 0.1 AutoMouseArea { id: ripple property color color: MSettings.darkTheme ? Qt.rgba(255, 255, 255, 0.16) : Qt.rgba(0, 0, 0, 0.08) property bool circular: false property bool centered: false property bool focused property color focusColor: "transparent" property int focusWidth: width - 32 property Item control clip: true Connections { target: control onPressedChanged: { if (!control.pressed) __private.removeLastCircle() } } onPressed: { __private.createTapCircle(mouse.x, mouse.y) if (control) mouse.accepted = false } onReleased: __private.removeLastCircle() onCanceled: __private.removeLastCircle() QtObject { id: __private property int startRadius: 0 property int endRadius property bool showFocus: true property Item lastCircle function createTapCircle(x, y) { endRadius = centered ? width/2 : radius(x, y) + 5 showFocus = false lastCircle = tapCircle.createObject(ripple, { "circleX": centered ? width/2 : x, "circleY": centered ? height/2 : y }) } function removeLastCircle() { if (lastCircle) lastCircle.removeCircle() } function radius(x, y) { var dist1 = Math.max(dist(x, y, 0, 0), dist(x, y, width, height)) var dist2 = Math.max(dist(x, y, width, 0), dist(x, y, 0, height)) return Math.max(dist1, dist2) } function dist(x1, y1, x2, y2) { var distX = x2 - x1 var distY = y2 - y1 return Math.sqrt(distX * distX + distY * distY) } } Rectangle { id: focusBackground objectName: "focusBackground" width: parent.width height: parent.height color: Qt.rgba(0,0,0,0.2) opacity: __private.showFocus && focused ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 500; easing.type: Easing.InOutQuad } } } Rectangle { id: focusCircle objectName: "focusRipple" property bool focusedState x: (parent.width - width)/2 y: (parent.height - height)/2 width: focused ? focusedState ? focusWidth : Math.min(parent.width - 8, focusWidth + 12) : parent.width/5 height: width radius: width/2 opacity: __private.showFocus && focused ? 1 : 0 color: focusColor.a === 0 ? Qt.rgba(1,1,1,0.4) : focusColor Behavior on opacity { NumberAnimation { duration: 500; easing.type: Easing.InOutQuad } } Behavior on width { NumberAnimation { duration: focusTimer.interval; } } Timer { id: focusTimer running: focused repeat: true interval: 800 onTriggered: focusCircle.focusedState = !focusCircle.focusedState } } Component { id: tapCircle Item { id: circleItem objectName: "tapRipple" property bool done property real circleX property real circleY property bool closed width: parent.width height: parent.height function removeCircle() { done = true if (fillSizeAnimation.running) { fillOpacityAnimation.stop() closeAnimation.start() circleItem.destroy(500); } else { __private.showFocus = true fadeAnimation.start(); circleItem.destroy(300); } } Item { id: circleParent width: parent.width height: parent.height visible: !circular Rectangle { id: circleRectangle x: circleItem.circleX - radius y: circleItem.circleY - radius width: radius * 2 height: radius * 2 opacity: 0 color: ripple.color NumberAnimation { id: fillSizeAnimation running: true target: circleRectangle; property: "radius"; duration: 500; from: __private.startRadius; to: __private.endRadius; easing.type: Easing.InOutQuad onStopped: { if (done) __private.showFocus = true } } NumberAnimation { id: fillOpacityAnimation running: true target: circleRectangle; property: "opacity"; duration: 300; from: 0; to: 1; easing.type: Easing.InOutQuad } NumberAnimation { id: fadeAnimation target: circleRectangle; property: "opacity"; duration: 300; from: 1; to: 0; easing.type: Easing.InOutQuad } SequentialAnimation { id: closeAnimation NumberAnimation { target: circleRectangle; property: "opacity"; duration: 250; to: 1; easing.type: Easing.InOutQuad } NumberAnimation { target: circleRectangle; property: "opacity"; duration: 250; from: 1; to: 0; easing.type: Easing.InOutQuad } } } } CircleMask { anchors.fill: parent source: circleParent visible: circular } } } } spectral/imports/Spectral/Effect/ElevationEffect.qml0000644000175000000620000001051413566674120022607 0ustar dilingerstaffimport QtQuick 2.12 import QtGraphicalEffects 1.0 /*! An effect for standard Material Design elevation shadows */ Item { id: effect property var source readonly property Item sourceItem: source.sourceItem property int elevation: 0 // Shadow details follow Material Design (taken from Angular Material) readonly property var _shadows: [ [{offset: 0, blur: 0, spread: 0}, {offset: 0, blur: 0, spread: 0}, {offset: 0, blur: 0, spread: 0}], [{offset: 1, blur: 3, spread: 0}, {offset: 1, blur: 1, spread: 0}, {offset: 2, blur: 1, spread: -1}], [{offset: 1, blur: 5, spread: 0}, {offset: 2, blur: 2, spread: 0}, {offset: 3, blur: 1, spread: -2}], [{offset: 1, blur: 8, spread: 0}, {offset: 3, blur: 4, spread: 0}, {offset: 3, blur: 3, spread: -2}], [{offset: 2, blur: 4, spread: -1}, {offset: 4, blur: 5, spread: 0}, {offset: 1, blur: 10, spread: 0}], [{offset: 3, blur: 5, spread: -1}, {offset: 5, blur: 8, spread: 0}, {offset: 1, blur: 14, spread: 0}], [{offset: 3, blur: 5, spread: -1}, {offset: 6, blur: 10, spread: 0}, {offset: 1, blur: 18, spread: 0}], [{offset: 4, blur: 5, spread: -2}, {offset: 7, blur: 10, spread: 1}, {offset: 2, blur: 16, spread: 1}], [{offset: 5, blur: 5, spread: -3}, {offset: 8, blur: 10, spread: 1}, {offset: 3, blur: 14, spread: 2}], [{offset: 5, blur: 6, spread: -3}, {offset: 9, blur: 12, spread: 1}, {offset: 3, blur: 16, spread: 2}], [{offset: 6, blur: 6, spread: -3}, {offset: 10, blur: 14, spread: 1}, {offset: 4, blur: 18, spread: 3}], [{offset: 6, blur: 7, spread: -4}, {offset: 11, blur: 15, spread: 1}, {offset: 4, blur: 20, spread: 3}], [{offset: 7, blur: 8, spread: -4}, {offset: 12, blur: 17, spread: 2}, {offset: 5, blur: 22, spread: 4}], [{offset: 7, blur: 8, spread: -4}, {offset: 13, blur: 19, spread: 2}, {offset: 5, blur: 24, spread: 4}], [{offset: 7, blur: 9, spread: -4}, {offset: 14, blur: 21, spread: 2}, {offset: 5, blur: 26, spread: 4}], [{offset: 8, blur: 9, spread: -5}, {offset: 15, blur: 22, spread: 2}, {offset: 6, blur: 28, spread: 5}], [{offset: 8, blur: 10, spread: -5}, {offset: 16, blur: 24, spread: 2}, {offset: 6, blur: 30, spread: 5}], [{offset: 8, blur: 11, spread: -5}, {offset: 17, blur: 26, spread: 2}, {offset: 6, blur: 32, spread: 5}], [{offset: 9, blur: 11, spread: -5}, {offset: 18, blur: 28, spread: 2}, {offset: 7, blur: 34, spread: 6}], [{offset: 9, blur: 12, spread: -6}, {offset: 19, blur: 29, spread: 2}, {offset: 7, blur: 36, spread: 6}], [{offset: 10, blur: 13, spread: -6}, {offset: 20, blur: 31, spread: 3}, {offset: 8, blur: 38, spread: 7}], [{offset: 10, blur: 13, spread: -6}, {offset: 21, blur: 33, spread: 3}, {offset: 8, blur: 40, spread: 7}], [{offset: 10, blur: 14, spread: -6}, {offset: 22, blur: 35, spread: 3}, {offset: 8, blur: 42, spread: 7}], [{offset: 11, blur: 14, spread: -7}, {offset: 23, blur: 36, spread: 3}, {offset: 9, blur: 44, spread: 8}], [{offset: 11, blur: 15, spread: -7}, {offset: 24, blur: 38, spread: 3}, {offset: 9, blur: 46, spread: 8}] ] readonly property var _shadowColors: [ Qt.rgba(0,0,0, 0.2), Qt.rgba(0,0,0, 0.14), Qt.rgba(0,0,0, 0.12) ] Repeater { model: _shadows[elevation] delegate: RectangularGlow { anchors { centerIn: parent verticalCenterOffset: modelData.offset } width: parent.width + 2 * modelData.spread height: parent.height + 2 * modelData.spread glowRadius: modelData.blur/2 spread: 0.05 color: _shadowColors[index] cornerRadius: modelData.blur + (effect.sourceItem.radius || 0) } } ShaderEffect { anchors.fill: parent property alias source: effect.source; } } spectral/imports/Spectral/Effect/qmldir0000644000175000000620000000014113566674120020237 0ustar dilingerstaffmodule Spectral.Effect ElevationEffect 2.0 ElevationEffect.qml RippleEffect 2.0 RippleEffect.qml spectral/imports/Spectral/Setting/0002755000175000000620000000000013566674120017253 5ustar dilingerstaffspectral/imports/Spectral/Setting/qmldir0000644000175000000620000000013713566674120020465 0ustar dilingerstaffmodule Spectral.Setting singleton MSettings 0.1 Setting.qml singleton MPalette 0.1 Palette.qml spectral/imports/Spectral/Setting/Palette.qml0000644000175000000620000000112613566674120021362 0ustar dilingerstaffpragma Singleton import QtQuick 2.12 import QtQuick.Controls.Material 2.12 QtObject { readonly property int theme: MSettings.darkTheme ? Material.Dark : Material.Light readonly property color primary: "#344955" readonly property color accent: "#4286F5" readonly property color foreground: MSettings.darkTheme ? "#FFFFFF" : "#1D333E" readonly property color background: MSettings.darkTheme ? "#303030" : "#FFFFFF" readonly property color lighter: MSettings.darkTheme ? "#FFFFFF" : "#5B7480" readonly property color banner: MSettings.darkTheme ? "#404040" : "#F2F3F4" } spectral/imports/Spectral/Setting/Setting.qml0000644000175000000620000000045013566674120021400 0ustar dilingerstaffpragma Singleton import QtQuick 2.12 import Qt.labs.settings 1.0 Settings { property bool showNotification: true property bool showTray: true property bool darkTheme property string fontFamily: "Roboto,Noto Sans,Noto Color Emoji" property bool markdownFormatting: true } spectral/imports/Spectral/Font/0002755000175000000620000000000013566674120016544 5ustar dilingerstaffspectral/imports/Spectral/Font/MaterialFont.qml0000644000175000000620000000014113566674120021636 0ustar dilingerstaffpragma Singleton import QtQuick 2.12 FontLoader { source: "qrc:/assets/font/material.ttf" } spectral/imports/Spectral/Font/qmldir0000644000175000000620000000010113566674120017745 0ustar dilingerstaffmodule Spectral.Font singleton MaterialFont 0.1 MaterialFont.qml spectral/imports/Spectral/Component/0002755000175000000620000000000013566674120017600 5ustar dilingerstaffspectral/imports/Spectral/Component/AutoRectangle.qml0000644000175000000620000000624413566674120023054 0ustar dilingerstaffimport QtQuick 2.12 Rectangle { property alias topLeftRadius: topLeftRect.radius property alias topRightRadius: topRightRect.radius property alias bottomLeftRadius: bottomLeftRect.radius property alias bottomRightRadius: bottomRightRect.radius property alias topLeftVisible: topLeftRect.visible property alias topRightVisible: topRightRect.visible property alias bottomLeftVisible: bottomLeftRect.visible property alias bottomRightVisible: bottomRightRect.visible antialiasing: true Rectangle { anchors.top: parent.top anchors.left: parent.left width: parent.width / 2 height: parent.height / 2 id: topLeftRect antialiasing: true color: parent.color Rectangle { anchors.bottom: parent.bottom anchors.left: parent.left width: parent.width / 2 height: parent.height / 2 color: parent.color } Rectangle { anchors.top: parent.top anchors.right: parent.right width: parent.width / 2 height: parent.height / 2 color: parent.color } } Rectangle { anchors.top: parent.top anchors.right: parent.right width: parent.width / 2 height: parent.height / 2 id: topRightRect antialiasing: true color: parent.color Rectangle { anchors.bottom: parent.bottom anchors.right: parent.right width: parent.width / 2 height: parent.height / 2 color: parent.color } Rectangle { anchors.top: parent.top anchors.left: parent.left width: parent.width / 2 height: parent.height / 2 color: parent.color } } Rectangle { anchors.bottom: parent.bottom anchors.left: parent.left width: parent.width / 2 height: parent.height / 2 id: bottomLeftRect antialiasing: true color: parent.color Rectangle { anchors.bottom: parent.bottom anchors.right: parent.right width: parent.width / 2 height: parent.height / 2 color: parent.color } Rectangle { anchors.top: parent.top anchors.left: parent.left width: parent.width / 2 height: parent.height / 2 color: parent.color } } Rectangle { anchors.bottom: parent.bottom anchors.right: parent.right width: parent.width / 2 height: parent.height / 2 id: bottomRightRect antialiasing: true color: parent.color Rectangle { anchors.bottom: parent.bottom anchors.left: parent.left width: parent.width / 2 height: parent.height / 2 color: parent.color } Rectangle { anchors.top: parent.top anchors.right: parent.right width: parent.width / 2 height: parent.height / 2 color: parent.color } } } spectral/imports/Spectral/Component/AutoTextField.qml0000644000175000000620000000502013566674120023027 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Controls.Material 2.3 TextField { id: textField selectByMouse: true topPadding: 8 bottomPadding: 8 background: Item { Label { id: floatingPlaceholder anchors.top: parent.top anchors.left: parent.left anchors.topMargin: textField.topPadding anchors.leftMargin: textField.leftPadding transformOrigin: Item.TopLeft visible: false color: Material.accent states: [ State { name: "shown" when: textField.text.length !== 0 || textField.activeFocus PropertyChanges { target: floatingPlaceholder; scale: 0.8 } PropertyChanges { target: floatingPlaceholder; anchors.topMargin: -floatingPlaceholder.height * 0.4 } } ] transitions: [ Transition { to: "shown" SequentialAnimation { PropertyAction { target: floatingPlaceholder; property: "text"; value: textField.placeholderText } PropertyAction { target: floatingPlaceholder; property: "visible"; value: true } PropertyAction { target: textField; property: "placeholderTextColor"; value: "transparent" } ParallelAnimation { NumberAnimation { target: floatingPlaceholder; property: "scale"; duration: 250; easing.type: Easing.InOutQuad } NumberAnimation { target: floatingPlaceholder; property: "anchors.topMargin"; duration: 250; easing.type: Easing.InOutQuad } } } }, Transition { from: "shown" SequentialAnimation { ParallelAnimation { NumberAnimation { target: floatingPlaceholder; property: "scale"; duration: 250; easing.type: Easing.InOutQuad } NumberAnimation { target: floatingPlaceholder; property: "anchors.topMargin"; duration: 250; easing.type: Easing.InOutQuad } } PropertyAction { target: textField; property: "placeholderTextColor"; value: "grey" } PropertyAction { target: floatingPlaceholder; property: "visible"; value: false } } } ] } } } spectral/imports/Spectral/Component/AutoMouseArea.qml0000644000175000000620000000045513566674120023027 0ustar dilingerstaffimport QtQuick 2.12 import Spectral.Setting 0.1 MouseArea { signal primaryClicked() signal secondaryClicked() acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: mouse.button == Qt.RightButton ? secondaryClicked() : primaryClicked() onPressAndHold: secondaryClicked() } spectral/imports/Spectral/Component/FullScreenImage.qml0000644000175000000620000000170513566674120023321 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 ApplicationWindow { property string filename property url localPath id: root flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground visible: true visibility: Qt.WindowFullScreen title: "Image View - " + filename color: "#BB000000" Shortcut { sequence: "Escape" onActivated: root.destroy() } AnimatedImage { anchors.centerIn: parent width: Math.min(sourceSize.width, root.width) height: Math.min(sourceSize.height, root.height) cache: false fillMode: Image.PreserveAspectFit source: localPath } ItemDelegate { anchors.top: parent.top anchors.right: parent.right id: closeButton width: 64 height: 64 contentItem: MaterialIcon { icon: "\ue5cd" color: "white" } onClicked: root.destroy() } } spectral/imports/Spectral/Component/Avatar.qml0000644000175000000620000000244313566674120021532 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtGraphicalEffects 1.0 import Spectral.Setting 0.1 Item { property string hint: "H" property string source: "" property color color: MPalette.accent readonly property url realSource: source ? "image://mxc/" + source : "" id: root Image { anchors.fill: parent id: image visible: realSource source: width < 1 ? "" : realSource sourceSize.width: width sourceSize.height: width fillMode: Image.PreserveAspectCrop mipmap: true layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { width: image.width height: image.width radius: width / 2 } } } Rectangle { anchors.fill: parent visible: !realSource || image.status != Image.Ready radius: height / 2 color: root.color antialiasing: true Label { anchors.centerIn: parent color: "white" text: hint[0].toUpperCase() font.pixelSize: root.width / 2 font.weight: Font.Medium horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } } spectral/imports/Spectral/Component/MaterialIcon.qml0000644000175000000620000000057613566674120022670 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import Spectral.Setting 0.1 import Spectral.Font 0.1 Text { property alias icon: materialLabel.text id: materialLabel color: MPalette.foreground font.pixelSize: 24 font.family: MaterialFont.name horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } spectral/imports/Spectral/Component/Emoji/0002755000175000000620000000000013566674120020643 5ustar dilingerstaffspectral/imports/Spectral/Component/Emoji/qmldir0000644000175000000620000000010013566674120022043 0ustar dilingerstaffmodule Spectral.Component.Emoji EmojiPicker 2.0 EmojiPicker.qml spectral/imports/Spectral/Component/Emoji/EmojiPicker.qml0000644000175000000620000000656713566674120023573 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import Spectral.Component 2.0 import Spectral 0.1 import Spectral.Setting 0.1 ColumnLayout { property string emojiCategory: "history" property var textArea property var emojiModel spacing: 0 ListView { Layout.fillWidth: true Layout.preferredHeight: 48 Layout.leftMargin: 24 Layout.rightMargin: 24 boundsBehavior: Flickable.DragOverBounds clip: true orientation: ListView.Horizontal model: ListModel { ListElement { label: "⌛️"; category: "history" } ListElement { label: "😏"; category: "people" } ListElement { label: "🌲"; category: "nature" } ListElement { label: "🍛"; category: "food"} ListElement { label: "🚁"; category: "activity" } ListElement { label: "🚅"; category: "travel" } ListElement { label: "💡"; category: "objects" } ListElement { label: "🔣"; category: "symbols" } ListElement { label: "🏁"; category: "flags" } } delegate: ItemDelegate { width: 64 height: 48 contentItem: Label { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pixelSize: 24 text: label } Rectangle { anchors.bottom: parent.bottom width: parent.width height: 2 visible: emojiCategory === category color: Material.accent } onClicked: emojiCategory = category } } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 1 Layout.leftMargin: 12 Layout.rightMargin: 12 color: MSettings.darkTheme ? "#424242" : "#e7ebeb" } GridView { Layout.fillWidth: true Layout.preferredHeight: 180 cellWidth: 48 cellHeight: 48 boundsBehavior: Flickable.DragOverBounds clip: true model: { switch (emojiCategory) { case "history": return emojiModel.history case "people": return emojiModel.people case "nature": return emojiModel.nature case "food": return emojiModel.food case "activity": return emojiModel.activity case "travel": return emojiModel.travel case "objects": return emojiModel.objects case "symbols": return emojiModel.symbols case "flags": return emojiModel.flags } return null } delegate: ItemDelegate { width: 48 height: 48 contentItem: Label { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pixelSize: 32 text: modelData.unicode } onClicked: { textArea.insert(textArea.cursorPosition, modelData.unicode) emojiModel.emojiUsed(modelData) } } ScrollBar.vertical: ScrollBar {} } } spectral/imports/Spectral/Component/ScrollHelper.qml0000644000175000000620000000752313566674120022716 0ustar dilingerstaff/* * Copyright (C) 2016 Michael Bohlender, * Copyright (C) 2017 Christian Mollekopf, * * 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 2 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, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.12 import QtQuick.Controls 2.12 /* * The MouseArea + interactive: false + maximumFlickVelocity are required * to fix scrolling for desktop systems where we don't want flicking behaviour. * * See also: * ScrollView.qml in qtquickcontrols * qquickwheelarea.cpp in qtquickcontrols */ MouseArea { id: root propagateComposedEvents: true property Flickable flickable property alias enabled: root.enabled //Place the mouse area under the flickable z: -1 onFlickableChanged: { if (enabled) { flickable.interactive = false flickable.maximumFlickVelocity = 100000 flickable.boundsBehavior = Flickable.StopAtBounds root.parent = flickable } } function calculateNewPosition(flickableItem, wheel) { //Nothing to scroll if (flickableItem.contentHeight < flickableItem.height) { return flickableItem.contentY; } //Ignore 0 events (happens at least with Christians trackpad) if (wheel.pixelDelta.y == 0 && wheel.angleDelta.y == 0) { return flickableItem.contentY; } //pixelDelta seems to be the same as angleDelta/8 var pixelDelta = 0 //The pixelDelta is a smaller number if both are provided, so pixelDelta can be 0 while angleDelta is still something. So we check the angleDelta if (wheel.angleDelta.y) { var wheelScrollLines = 3 //Default value of QApplication wheelScrollLines property var pixelPerLine = 20 //Default value in Qt, originally comes from QTextEdit var ticks = (wheel.angleDelta.y / 8) / 15.0 //Divide by 8 gives us pixels typically come in 15pixel steps. pixelDelta = ticks * pixelPerLine * wheelScrollLines } else { pixelDelta = wheel.pixelDelta.y } if (!pixelDelta) { return flickableItem.contentY; } var minYExtent = flickableItem.originY + flickableItem.topMargin; var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height; if (typeof(flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) { minYExtent += flickableItem.headerItem.height } //Avoid overscrolling return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta)); } onWheel: { var newPos = calculateNewPosition(flickable, wheel); // console.warn("Delta: ", wheel.pixelDelta.y); // console.warn("Old position: ", flickable.contentY); // console.warn("New position: ", newPos); // Show the scrollbars flickable.flick(0, 0); flickable.contentY = newPos; cancelFlickStateTimer.start() } Timer { id: cancelFlickStateTimer //How long the scrollbar will remain visible interval: 500 // Hide the scrollbars onTriggered: flickable.cancelFlick(); } } spectral/imports/Spectral/Component/Timeline/0002755000175000000620000000000013566674120021346 5ustar dilingerstaffspectral/imports/Spectral/Component/Timeline/StateDelegate.qml0000644000175000000620000000421413566674120024573 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import Spectral.Component 2.0 import Spectral.Dialog 2.0 import Spectral.Effect 2.0 import Spectral.Setting 0.1 Control { id: root padding: 8 contentItem: RowLayout { id: row Control { Layout.alignment: Qt.AlignTop id: authorControl padding: 4 background: Rectangle { radius: height / 2 color: author.color } contentItem: RowLayout { Avatar { Layout.preferredWidth: 24 Layout.preferredHeight: 24 hint: author.displayName source: author.avatarMediaId color: Qt.darker(author.color, 1.1) Component { id: userDetailDialog UserDetailDialog {} } RippleEffect { anchors.fill: parent circular: true onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open() } } Label { Layout.alignment: Qt.AlignVCenter text: author.displayName font.pixelSize: 13 font.weight: Font.Medium color: "white" rightPadding: 8 } } } Label { Layout.fillWidth: true Layout.maximumWidth: messageListView.width - authorControl.width - row.spacing - (root.padding * 2) text: display + " • " + Qt.formatTime(time, "hh:mm AP") color: MPalette.foreground font.pixelSize: 13 font.weight: Font.Medium wrapMode: Label.Wrap onLinkActivated: Qt.openUrlExternally(link) } } } spectral/imports/Spectral/Component/Timeline/FileDelegate.qml0000644000175000000620000001641013566674120024373 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import QtGraphicalEffects 1.0 import Qt.labs.platform 1.0 as Platform import Spectral 0.1 import Spectral.Setting 0.1 import Spectral.Component 2.0 import Spectral.Dialog 2.0 import Spectral.Menu.Timeline 2.0 import Spectral.Font 0.1 import Spectral.Effect 2.0 RowLayout { readonly property bool avatarVisible: !sentByMe && showAuthor readonly property bool sentByMe: author.isLocalUser property bool openOnFinished: false readonly property bool downloaded: progressInfo && progressInfo.completed id: root spacing: 4 onDownloadedChanged: if (downloaded && openOnFinished) openSavedFile() z: -5 Avatar { Layout.preferredWidth: 36 Layout.preferredHeight: 36 Layout.alignment: Qt.AlignBottom visible: avatarVisible hint: author.displayName source: author.avatarMediaId color: author.color Component { id: userDetailDialog UserDetailDialog {} } RippleEffect { anchors.fill: parent circular: true onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open() } } Item { Layout.preferredWidth: 36 Layout.preferredHeight: 36 visible: !(sentByMe || avatarVisible) } Control { Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + root.spacing : 0) - 48 padding: 12 contentItem: RowLayout { ToolButton { contentItem: MaterialIcon { icon: progressInfo.completed ? "\ue5ca" : "\ue2c4" } onClicked: progressInfo.completed ? openSavedFile() : saveFileAs() } ColumnLayout { Label { Layout.fillWidth: true text: display color: MPalette.foreground wrapMode: Label.Wrap font.pixelSize: 18 font.weight: Font.Medium font.capitalization: Font.AllUppercase } Label { Layout.fillWidth: true text: !progressInfo.completed && progressInfo.active ? (humanSize(progressInfo.progress) + "/" + humanSize(progressInfo.total)) : humanSize(content.info ? content.info.size : 0) color: MPalette.lighter wrapMode: Label.Wrap } } } background: Rectangle { color: MPalette.background radius: 18 Rectangle { anchors.top: parent.top anchors.left: parent.left width: parent.width / 2 height: parent.height / 2 visible: !sentByMe && (bubbleShape == 3 || bubbleShape == 2) color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent radius: 2 } Rectangle { anchors.top: parent.top anchors.right: parent.right width: parent.width / 2 height: parent.height / 2 visible: sentByMe && (bubbleShape == 3 || bubbleShape == 2) color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent radius: 2 } Rectangle { anchors.bottom: parent.bottom anchors.left: parent.left width: parent.width / 2 height: parent.height / 2 visible: !sentByMe && (bubbleShape == 1 || bubbleShape == 2) color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent radius: 2 } Rectangle { anchors.bottom: parent.bottom anchors.right: parent.right width: parent.width / 2 height: parent.height / 2 visible: sentByMe && (bubbleShape == 1 || bubbleShape == 2) color: sentByMe ? MPalette.background : eventType === "notice" ? MPalette.primary : MPalette.accent radius: 2 } AutoMouseArea { anchors.fill: parent id: messageMouseArea onSecondaryClicked: { var contextMenu = fileDelegateContextMenu.createObject(root) contextMenu.viewSource.connect(function() { messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open() }) contextMenu.downloadAndOpen.connect(downloadAndOpen) contextMenu.saveFileAs.connect(saveFileAs) contextMenu.reply.connect(function() { roomPanelInput.replyModel = Object.assign({}, model) roomPanelInput.isReply = true roomPanelInput.focus() }) contextMenu.redact.connect(function() { currentRoom.redactEvent(eventId) }) contextMenu.popup() } Component { id: messageSourceDialog MessageSourceDialog {} } Component { id: openFolderDialog OpenFolderDialog {} } Component { id: fileDelegateContextMenu FileDelegateContextMenu {} } } } } function saveFileAs() { var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay) folderDialog.chosen.connect(function(path) { if (!path) return currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId)) }) folderDialog.open() } function downloadAndOpen() { if (downloaded) openSavedFile() else { openOnFinished = true currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) } } function openSavedFile() { if (Qt.openUrlExternally(progressInfo.localPath)) return; if (Qt.openUrlExternally(progressInfo.localDir)) return; } function humanSize(bytes) { if (!bytes) return qsTr("Unknown", "Unknown attachment size") if (bytes < 4000) return qsTr("%1 bytes").arg(bytes) bytes = Math.round(bytes / 100) / 10 if (bytes < 2000) return qsTr("%1 KB").arg(bytes) bytes = Math.round(bytes / 100) / 10 if (bytes < 2000) return qsTr("%1 MB").arg(bytes) return qsTr("%1 GB").arg(Math.round(bytes / 100) / 10) } } spectral/imports/Spectral/Component/Timeline/AudioDelegate.qml0000644000175000000620000001442013566674120024554 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import QtGraphicalEffects 1.0 import Qt.labs.platform 1.0 as Platform import QtMultimedia 5.12 import Spectral 0.1 import Spectral.Setting 0.1 import Spectral.Component 2.0 import Spectral.Dialog 2.0 import Spectral.Menu.Timeline 2.0 import Spectral.Font 0.1 import Spectral.Effect 2.0 RowLayout { readonly property bool avatarVisible: !sentByMe && showAuthor readonly property bool sentByMe: author.isLocalUser id: root spacing: 4 z: -5 Avatar { Layout.preferredWidth: 36 Layout.preferredHeight: 36 Layout.alignment: Qt.AlignBottom visible: avatarVisible hint: author.displayName source: author.avatarMediaId color: author.color Component { id: userDetailDialog UserDetailDialog {} } RippleEffect { anchors.fill: parent circular: true onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open() } } Item { Layout.preferredWidth: 36 Layout.preferredHeight: 36 visible: !(sentByMe || avatarVisible) } Control { Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + root.spacing : 0) - 48 padding: 12 Audio { id: audio source: currentRoom.urlToMxcUrl(content.url) } contentItem: RowLayout { ToolButton { contentItem: MaterialIcon { icon: audio.playbackState == Audio.PlayingState ? "\ue034" : "\ue405" } onClicked: { if (audio.playbackState == Audio.PlayingState) { audio.pause() } else { audio.play() } } } ColumnLayout { Label { Layout.fillWidth: true text: display color: MPalette.foreground wrapMode: Label.Wrap font.pixelSize: 18 font.weight: Font.Medium font.capitalization: Font.AllUppercase } Label { readonly property int duration: content.info.duration || audio.duration || 0 Layout.fillWidth: true visible: duration text: humanSize(duration) color: MPalette.lighter wrapMode: Label.Wrap } } } background: AutoRectangle { readonly property int minorRadius: 8 id: bubbleBackground color: MPalette.background radius: 18 topLeftVisible: !sentByMe && (bubbleShape == 3 || bubbleShape == 2) topRightVisible: sentByMe && (bubbleShape == 3 || bubbleShape == 2) bottomLeftVisible: !sentByMe && (bubbleShape == 1 || bubbleShape == 2) bottomRightVisible: sentByMe && (bubbleShape == 1 || bubbleShape == 2) topLeftRadius: minorRadius topRightRadius: minorRadius bottomLeftRadius: minorRadius bottomRightRadius: minorRadius AutoMouseArea { anchors.fill: parent id: messageMouseArea onSecondaryClicked: { var contextMenu = fileDelegateContextMenu.createObject(root) contextMenu.viewSource.connect(function() { messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open() }) contextMenu.downloadAndOpen.connect(downloadAndOpen) contextMenu.saveFileAs.connect(saveFileAs) contextMenu.reply.connect(function() { roomPanelInput.replyModel = Object.assign({}, model) roomPanelInput.isReply = true roomPanelInput.focus() }) contextMenu.redact.connect(function() { currentRoom.redactEvent(eventId) }) contextMenu.popup() } Component { id: messageSourceDialog MessageSourceDialog {} } Component { id: openFolderDialog OpenFolderDialog {} } Component { id: fileDelegateContextMenu FileDelegateContextMenu {} } } } } function saveFileAs() { var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay) folderDialog.chosen.connect(function(path) { if (!path) return currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId)) }) folderDialog.open() } function downloadAndOpen() { if (downloaded) openSavedFile() else { openOnFinished = true currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) } } function openSavedFile() { if (Qt.openUrlExternally(progressInfo.localPath)) return; if (Qt.openUrlExternally(progressInfo.localDir)) return; } function humanSize(duration) { if (!duration) return qsTr("Unknown", "Unknown duration") if (duration < 1000) return qsTr("An instant") duration = Math.round(duration / 100) / 10 if (duration < 60) return qsTr("%1 sec.").arg(duration) duration = Math.round(duration / 6) / 10 if (duration < 60) return qsTr("%1 min.").arg(duration) return qsTr("%1 hrs.").arg(Math.round(duration / 6) / 10) } } spectral/imports/Spectral/Component/Timeline/SectionDelegate.qml0000644000175000000620000000051413566674120025116 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import Spectral.Setting 0.1 Label { text: section + " • " + Qt.formatTime(time, "hh:mm AP") color: MPalette.foreground font.pixelSize: 13 font.weight: Font.Medium font.capitalization: Font.AllUppercase verticalAlignment: Text.AlignVCenter padding: 8 } spectral/imports/Spectral/Component/Timeline/ImageDelegate.qml0000644000175000000620000001443713566674120024545 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import QtGraphicalEffects 1.0 import Qt.labs.platform 1.0 as Platform import Spectral 0.1 import Spectral.Setting 0.1 import Spectral.Component 2.0 import Spectral.Dialog 2.0 import Spectral.Menu.Timeline 2.0 import Spectral.Effect 2.0 import Spectral.Font 0.1 RowLayout { readonly property bool avatarVisible: showAuthor && !sentByMe readonly property bool sentByMe: author.isLocalUser readonly property bool isAnimated: contentType === "image/gif" property bool openOnFinished: false readonly property bool downloaded: progressInfo && progressInfo.completed id: root spacing: 4 z: -5 onDownloadedChanged: { if (downloaded && openOnFinished) { openSavedFile() openOnFinished = false } } Avatar { Layout.preferredWidth: 36 Layout.preferredHeight: 36 Layout.alignment: Qt.AlignBottom visible: avatarVisible hint: author.displayName source: author.avatarMediaId color: author.color Component { id: userDetailDialog UserDetailDialog {} } RippleEffect { anchors.fill: parent circular: true onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open() } } Item { Layout.preferredWidth: 36 Layout.preferredHeight: 36 visible: !(sentByMe || avatarVisible) } Image { readonly property bool isThumbnail: !(content.info.thumbnail_info == null || content.thumbnailMediaId == null) readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info readonly property string mediaId: isThumbnail ? content.thumbnailMediaId : content.mediaId readonly property int maxWidth: messageListView.width - (!sentByMe ? 36 + root.spacing : 0) - 48 Layout.minimumWidth: 256 Layout.minimumHeight: 64 Layout.preferredWidth: info.w > maxWidth ? maxWidth : info.w Layout.preferredHeight: info.w > maxWidth ? (info.h / info.w * maxWidth) : info.h id: img source: "image://mxc/" + mediaId sourceSize.width: info.w sourceSize.height: info.h fillMode: Image.PreserveAspectCrop layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { width: img.width height: img.height radius: 18 } } Control { anchors.bottom: parent.bottom anchors.bottomMargin: 8 anchors.right: parent.right anchors.rightMargin: 8 horizontalPadding: 8 verticalPadding: 4 contentItem: RowLayout { Label { text: Qt.formatTime(time, "hh:mm AP") color: "white" font.pixelSize: 12 } Label { text: author.displayName color: "white" font.pixelSize: 12 } } background: Rectangle { radius: height / 2 color: "black" opacity: 0.3 } } Rectangle { anchors.fill: parent visible: progressInfo.active && !downloaded color: "#BB000000" ProgressBar { anchors.centerIn: parent width: parent.width * 0.8 from: 0 to: progressInfo.total value: progressInfo.progress } } RippleEffect { anchors.fill: parent id: messageMouseArea onPrimaryClicked: fullScreenImage.createObject(parent, {"filename": eventId, "localPath": currentRoom.urlToDownload(eventId)}).showFullScreen() onSecondaryClicked: { var contextMenu = imageDelegateContextMenu.createObject(root) contextMenu.viewSource.connect(function() { messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open() }) contextMenu.downloadAndOpen.connect(downloadAndOpen) contextMenu.saveFileAs.connect(saveFileAs) contextMenu.reply.connect(function() { roomPanelInput.replyModel = Object.assign({}, model) roomPanelInput.isReply = true roomPanelInput.focus() }) contextMenu.redact.connect(function() { currentRoom.redactEvent(eventId) }) contextMenu.popup() } Component { id: messageSourceDialog MessageSourceDialog {} } Component { id: openFolderDialog OpenFolderDialog {} } Component { id: imageDelegateContextMenu FileDelegateContextMenu {} } Component { id: fullScreenImage FullScreenImage {} } } } function saveFileAs() { var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay) folderDialog.chosen.connect(function(path) { if (!path) return currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId)) }) folderDialog.open() } function downloadAndOpen() { if (downloaded) openSavedFile() else { openOnFinished = true currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) } } function openSavedFile() { if (Qt.openUrlExternally(progressInfo.localPath)) return; if (Qt.openUrlExternally(progressInfo.localDir)) return; } } spectral/imports/Spectral/Component/Timeline/VideoDelegate.qml0000644000175000000620000002026613566674120024566 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import QtGraphicalEffects 1.0 import QtMultimedia 5.12 import Qt.labs.platform 1.0 as Platform import Spectral 0.1 import Spectral.Setting 0.1 import Spectral.Component 2.0 import Spectral.Dialog 2.0 import Spectral.Menu.Timeline 2.0 import Spectral.Effect 2.0 import Spectral.Font 0.1 RowLayout { readonly property bool avatarVisible: showAuthor && !sentByMe readonly property bool sentByMe: author.isLocalUser property bool openOnFinished: false property bool playOnFinished: false readonly property bool downloaded: progressInfo && progressInfo.completed property bool supportStreaming: true id: root spacing: 4 z: -5 onDownloadedChanged: { if (downloaded) { vid.source = progressInfo.localPath } if (downloaded && openOnFinished) { openSavedFile() openOnFinished = false } if (downloaded && playOnFinished) { playSavedFile() playOnFinished = false } } Avatar { Layout.preferredWidth: 36 Layout.preferredHeight: 36 Layout.alignment: Qt.AlignBottom visible: avatarVisible hint: author.displayName source: author.avatarMediaId color: author.color Component { id: userDetailDialog UserDetailDialog {} } RippleEffect { anchors.fill: parent circular: true onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open() } } Item { Layout.preferredWidth: 36 Layout.preferredHeight: 36 visible: !(sentByMe || avatarVisible) } Video { readonly property int maxWidth: messageListView.width - (!sentByMe ? 36 + root.spacing : 0) - 48 Layout.preferredWidth: content.info.w > maxWidth ? maxWidth : content.info.w Layout.preferredHeight: content.info.w > maxWidth ? (content.info.h / content.info.w * maxWidth) : content.info.h id: vid loops: MediaPlayer.Infinite fillMode: VideoOutput.PreserveAspectFit Component.onCompleted: { if (downloaded) { source = progressInfo.localPath } else { source = currentRoom.urlToMxcUrl(content.url) } } onDurationChanged: { if (!duration) { supportStreaming = false; } } onErrorChanged: { if (error != MediaPlayer.NoError) { supportStreaming = false; } } layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { width: vid.width height: vid.height radius: 18 } } Image { readonly property bool isThumbnail: !(content.info.thumbnail_info == null || content.thumbnailMediaId == null) readonly property var info: isThumbnail ? content.info.thumbnail_info : content.info anchors.fill: parent visible: isThumbnail && (vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError) source: "image://mxc/" + (isThumbnail ? content.thumbnailMediaId : "") sourceSize.width: info.w sourceSize.height: info.h fillMode: Image.PreserveAspectCrop } Label { anchors.centerIn: parent visible: vid.playbackState == MediaPlayer.StoppedState || vid.error != MediaPlayer.NoError color: "white" text: "Video" font.pixelSize: 16 padding: 8 background: Rectangle { radius: height / 2 color: "black" opacity: 0.3 } } Control { anchors.bottom: parent.bottom anchors.bottomMargin: 8 anchors.right: parent.right anchors.rightMargin: 8 horizontalPadding: 8 verticalPadding: 4 contentItem: RowLayout { Label { text: Qt.formatTime(time, "hh:mm AP") color: "white" font.pixelSize: 12 } Label { text: author.displayName color: "white" font.pixelSize: 12 } } background: Rectangle { radius: height / 2 color: "black" opacity: 0.3 } } Rectangle { anchors.fill: parent visible: progressInfo.active && !downloaded color: "#BB000000" ProgressBar { anchors.centerIn: parent width: parent.width * 0.8 from: 0 to: progressInfo.total value: progressInfo.progress } } RippleEffect { anchors.fill: parent id: messageMouseArea onPrimaryClicked: { if (supportStreaming || progressInfo.completed) { if (vid.playbackState == MediaPlayer.PlayingState) { vid.pause() } else { vid.play() } } else { downloadAndPlay() } } onSecondaryClicked: { var contextMenu = imageDelegateContextMenu.createObject(root) contextMenu.viewSource.connect(function() { messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open() }) contextMenu.downloadAndOpen.connect(downloadAndOpen) contextMenu.saveFileAs.connect(saveFileAs) contextMenu.reply.connect(function() { roomPanelInput.replyModel = Object.assign({}, model) roomPanelInput.isReply = true roomPanelInput.focus() }) contextMenu.redact.connect(function() { currentRoom.redactEvent(eventId) }) contextMenu.popup() } Component { id: messageSourceDialog MessageSourceDialog {} } Component { id: openFolderDialog OpenFolderDialog {} } Component { id: imageDelegateContextMenu FileDelegateContextMenu {} } } } function saveFileAs() { var folderDialog = openFolderDialog.createObject(ApplicationWindow.overlay) folderDialog.chosen.connect(function(path) { if (!path) return currentRoom.downloadFile(eventId, path + "/" + currentRoom.fileNameToDownload(eventId)) }) folderDialog.open() } function downloadAndOpen() { if (downloaded) openSavedFile() else { openOnFinished = true currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) } } function downloadAndPlay() { if (downloaded) playSavedFile() else { playOnFinished = true currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + currentRoom.fileNameToDownload(eventId)) } } function openSavedFile() { if (Qt.openUrlExternally(progressInfo.localPath)) return; if (Qt.openUrlExternally(progressInfo.localDir)) return; } function playSavedFile() { vid.stop() vid.play() } } spectral/imports/Spectral/Component/Timeline/qmldir0000644000175000000620000000051713566674120022562 0ustar dilingerstaffmodule Spectral.Component.Timeline MessageDelegate 2.0 MessageDelegate.qml StateDelegate 2.0 StateDelegate.qml SectionDelegate 2.0 SectionDelegate.qml ImageDelegate 2.0 ImageDelegate.qml FileDelegate 2.0 FileDelegate.qml VideoDelegate 2.0 VideoDelegate.qml ReactionDelegate 2.0 ReactionDelegate.qml AudioDelegate 2.0 AudioDelegate.qml spectral/imports/Spectral/Component/Timeline/MessageDelegate.qml0000644000175000000620000002515013566674120025101 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import Spectral 0.1 import Spectral.Setting 0.1 import Spectral.Component 2.0 import Spectral.Dialog 2.0 import Spectral.Menu.Timeline 2.0 import Spectral.Effect 2.0 ColumnLayout { readonly property bool avatarVisible: !sentByMe && showAuthor readonly property bool sentByMe: author.isLocalUser readonly property bool darkBackground: !sentByMe readonly property bool replyVisible: reply || false readonly property bool failed: marks === EventStatus.SendingFailed readonly property color authorColor: eventType === "notice" ? MPalette.primary : author.color readonly property color replyAuthorColor: replyVisible ? reply.author.color : MPalette.accent signal saveFileAs() signal openExternally() id: root z: -5 spacing: 0 RowLayout { Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft id: messageRow spacing: 4 Avatar { Layout.preferredWidth: 36 Layout.preferredHeight: 36 Layout.alignment: Qt.AlignBottom visible: avatarVisible hint: author.displayName source: author.avatarMediaId color: author.color Component { id: userDetailDialog UserDetailDialog {} } RippleEffect { anchors.fill: parent circular: true onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author.object, "displayName": author.displayName, "avatarMediaId": author.avatarMediaId, "avatarUrl": author.avatarUrl}).open() } } Item { Layout.preferredWidth: 36 Layout.preferredHeight: 36 visible: !(sentByMe || avatarVisible) } Control { Layout.maximumWidth: messageListView.width - (!sentByMe ? 36 + messageRow.spacing : 0) - 48 Layout.minimumHeight: 36 padding: 0 background: AutoRectangle { readonly property int minorRadius: 8 id: bubbleBackground color: sentByMe ? MPalette.background : authorColor radius: 18 topLeftVisible: !sentByMe && (bubbleShape == 3 || bubbleShape == 2) topRightVisible: sentByMe && (bubbleShape == 3 || bubbleShape == 2) bottomLeftVisible: !sentByMe && (bubbleShape == 1 || bubbleShape == 2) bottomRightVisible: sentByMe && (bubbleShape == 1 || bubbleShape == 2) topLeftRadius: minorRadius topRightRadius: minorRadius bottomLeftRadius: minorRadius bottomRightRadius: minorRadius AutoMouseArea { anchors.fill: parent id: messageMouseArea onSecondaryClicked: { var contextMenu = messageDelegateContextMenu.createObject(root) contextMenu.viewSource.connect(function() { messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open() }) contextMenu.reply.connect(function() { roomPanelInput.replyModel = Object.assign({}, model) roomPanelInput.isReply = true roomPanelInput.focus() }) contextMenu.redact.connect(function() { currentRoom.redactEvent(eventId) }) contextMenu.popup() } Component { id: messageDelegateContextMenu MessageDelegateContextMenu {} } Component { id: messageSourceDialog MessageSourceDialog {} } } } contentItem: ColumnLayout { spacing: 0 Control { Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 8 Layout.rightMargin: 8 id: replyControl padding: 4 rightPadding: 12 visible: replyVisible contentItem: RowLayout { Avatar { Layout.preferredWidth: 28 Layout.preferredHeight: 28 Layout.alignment: Qt.AlignTop source: replyVisible ? reply.author.avatarMediaId : "" hint: replyVisible ? reply.author.displayName : "H" color: replyVisible ? reply.author.color : MPalette.accent RippleEffect { anchors.fill: parent circular: true onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": reply.author.object, "displayName": reply.author.displayName, "avatarMediaId": reply.author.avatarMediaId, "avatarUrl": reply.author.avatarUrl}).open() } } TextEdit { Layout.fillWidth: true color: !sentByMe ? MPalette.foreground : "white" text: "" + (replyVisible ? reply.display : "") font.family: window.font.family selectByMouse: true readOnly: true wrapMode: Label.Wrap selectedTextColor: darkBackground ? "white" : replyAuthorColor selectionColor: darkBackground ? replyAuthorColor : "white" textFormat: Text.RichText } } background: Rectangle { color: sentByMe ? replyAuthorColor : MPalette.background radius: 18 AutoMouseArea { anchors.fill: parent onClicked: goToEvent(reply.eventId) } } } TextEdit { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.topMargin: 8 Layout.bottomMargin: 8 id: contentLabel text: "" + display color: darkBackground ? "white" : MPalette.foreground font.family: window.font.family font.pixelSize: (message.length === 2 && /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g.test(message)) ? 48 : 14 selectByMouse: true readOnly: true wrapMode: Label.Wrap selectedTextColor: darkBackground ? authorColor : "white" selectionColor: darkBackground ? "white" : authorColor textFormat: Text.RichText onLinkActivated: { if (link.startsWith("https://matrix.to/")) { var result = link.replace(/\?.*/, "").match("https://matrix.to/#/(!.*:.*)/(\\$.*:.*)") if (!result || result.length < 3) return if (result[1] != currentRoom.id) return if (!result[2]) return goToEvent(result[2]) } else { Qt.openUrlExternally(link) } } MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor } } ReactionDelegate { Layout.fillWidth: true Layout.topMargin: 0 Layout.bottomMargin: 8 Layout.leftMargin: 16 Layout.rightMargin: 16 } } } } RowLayout { Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft Layout.leftMargin: sentByMe ? undefined : 36 + messageRow.spacing + 12 Layout.rightMargin: sentByMe ? 12 : undefined Layout.bottomMargin: 4 visible: showAuthor && !failed Label { visible: !sentByMe text: author.displayName color: MPalette.lighter } Label { text: Qt.formatTime(time, "hh:mm AP") color: MPalette.lighter } } RowLayout { Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft Layout.leftMargin: sentByMe ? undefined : 36 + messageRow.spacing + 12 Layout.rightMargin: sentByMe ? 12 : undefined Layout.bottomMargin: 4 visible: failed Label { text: "Send failed:" color: MPalette.lighter } Label { text: "Resend" color: MPalette.lighter MouseArea { anchors.fill: parent onClicked: currentRoom.retryMessage(eventId) } } Label { text: "|" color: MPalette.lighter } Label { text: "Discard" color: MPalette.lighter MouseArea { anchors.fill: parent onClicked: currentRoom.discardMessage(eventId) } } } } spectral/imports/Spectral/Component/Timeline/ReactionDelegate.qml0000644000175000000620000000337413566674120025265 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import Spectral.Setting 0.1 Flow { visible: (reaction && reaction.length > 0) || false spacing: 8 Repeater { model: reaction delegate: Control { width: Math.min(implicitWidth, 128) horizontalPadding: 6 verticalPadding: 0 contentItem: Label { text: modelData.reaction + (modelData.count > 1 ? " " + modelData.count : "") color: MPalette.lighter font.pixelSize: 14 elide: Text.ElideRight } background: Rectangle { radius: height / 2 color: modelData.hasLocalUser ? (MSettings.darkTheme ? Qt.darker(MPalette.accent, 1.55) : Qt.lighter(MPalette.accent, 1.55)) : MPalette.banner MouseArea { anchors.fill: parent hoverEnabled: true ToolTip.visible: containsMouse ToolTip.text: { var text = ""; for (var i = 0; i < modelData.authors.length; i++) { if (i === modelData.authors.length - 1 && i !== 0) { text += " and " } else if (i !== 0) { text += ", " } text += modelData.authors[i].displayName } text += " reacted with " + modelData.reaction return text } onClicked: currentRoom.toggleReaction(eventId, modelData.reaction) } } } } } spectral/imports/Spectral/Component/qmldir0000644000175000000620000000051613566674120021013 0ustar dilingerstaffmodule Spectral.Component AutoMouseArea 2.0 AutoMouseArea.qml MaterialIcon 2.0 MaterialIcon.qml SideNavButton 2.0 SideNavButton.qml ScrollHelper 2.0 ScrollHelper.qml AutoListView 2.0 AutoListView.qml AutoTextField 2.0 AutoTextField.qml Avatar 2.0 Avatar.qml FullScreenImage 2.0 FullScreenImage.qml AutoRectangle 2.0 AutoRectangle.qml spectral/imports/Spectral/Component/AutoListView.qml0000644000175000000620000000021313566674120022704 0ustar dilingerstaffimport QtQuick 2.12 ListView { pixelAligned: true ScrollHelper { anchors.fill: parent flickable: parent } } spectral/imports/Spectral/Dialog/0002755000175000000620000000000013566674120017035 5ustar dilingerstaffspectral/imports/Spectral/Dialog/InviteUserDialog.qml0000644000175000000620000000076013566674120022766 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import Spectral.Component 2.0 Dialog { property var room anchors.centerIn: parent width: 360 id: root title: "Invite User" modal: true standardButtons: Dialog.Ok | Dialog.Cancel contentItem: AutoTextField { id: inviteUserDialogTextField placeholderText: "User ID" } onAccepted: room.inviteToRoom(inviteUserDialogTextField.text) onClosed: destroy() } spectral/imports/Spectral/Dialog/LoginDialog.qml0000644000175000000620000000341513566674120021741 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import Spectral.Component 2.0 Dialog { anchors.centerIn: parent width: 360 id: root title: "Login" standardButtons: Dialog.Ok | Dialog.Cancel onAccepted: doLogin() contentItem: ColumnLayout { AutoTextField { Layout.fillWidth: true id: serverField placeholderText: "Server Address" text: "https://matrix.org" } AutoTextField { Layout.fillWidth: true id: usernameField placeholderText: "Username" onAccepted: passwordField.forceActiveFocus() } AutoTextField { Layout.fillWidth: true id: passwordField placeholderText: "Password" echoMode: TextInput.Password onAccepted: accessTokenField.forceActiveFocus() } AutoTextField { Layout.fillWidth: true id: accessTokenField placeholderText: "Access Token (Optional)" onAccepted: deviceNameField.forceActiveFocus() } AutoTextField { Layout.fillWidth: true id: deviceNameField placeholderText: "Device Name (Optional)" onAccepted: root.accept() } } function doLogin() { if (accessTokenField.text !== "") { console.log("Login using access token.") spectralController.loginWithAccessToken(serverField.text, usernameField.text, accessTokenField.text, deviceNameField.text) } else { spectralController.loginWithCredentials(serverField.text, usernameField.text, passwordField.text, deviceNameField.text) } } onClosed: destroy() } spectral/imports/Spectral/Dialog/FontFamilyDialog.qml0000644000175000000620000000104213566674120022733 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import Spectral.Component 2.0 import Spectral.Setting 0.1 Dialog { anchors.centerIn: parent width: 360 id: root title: "Enter Font Family" contentItem: AutoTextField { Layout.fillWidth: true id:fontFamilyField text: MSettings.fontFamily placeholderText: "Font Family" } standardButtons: Dialog.Ok | Dialog.Cancel onAccepted: MSettings.fontFamily = fontFamilyField.text onClosed: destroy() } spectral/imports/Spectral/Dialog/RoomSettingsDialog.qml0000644000175000000620000001647313566674120023336 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import Spectral.Component 2.0 import Spectral.Effect 2.0 import Spectral.Setting 0.1 Dialog { property var room anchors.centerIn: parent width: 480 id: root title: "Room Settings - " + room.displayName modal: true contentItem: ColumnLayout { RowLayout { Layout.fillWidth: true spacing: 16 Avatar { Layout.preferredWidth: 72 Layout.preferredHeight: 72 Layout.alignment: Qt.AlignTop hint: room.displayName source: room.avatarMediaId RippleEffect { anchors.fill: parent circular: true onClicked: { var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay) fileDialog.chosen.connect(function(path) { if (!path) return room.changeAvatar(path) }) fileDialog.open() } } } ColumnLayout { Layout.fillWidth: true Layout.margins: 4 AutoTextField { Layout.fillWidth: true id: roomNameField text: room.name placeholderText: "Room Name" } AutoTextField { Layout.fillWidth: true id: roomTopicField text: room.topic placeholderText: "Room Topic" } } } Control { Layout.fillWidth: true visible: room.predecessorId && room.connection.room(room.predecessorId) padding: 8 contentItem: RowLayout { MaterialIcon { Layout.preferredWidth: 48 Layout.preferredHeight: 48 icon: "\ue8d4" } ColumnLayout { Layout.fillWidth: true spacing: 0 Label { Layout.fillWidth: true font.bold: true color: MPalette.foreground text: "This room is a continuation of another conversation." } Label { Layout.fillWidth: true color: MPalette.lighter text: "Click here to see older messages." } } } background: Rectangle { color: MPalette.banner RippleEffect { anchors.fill: parent onClicked: { roomListForm.enteredRoom = spectralController.connection.room(room.predecessorId) root.close() } } } } Control { Layout.fillWidth: true visible: room.successorId && room.connection.room(room.successorId) padding: 8 contentItem: RowLayout { MaterialIcon { Layout.preferredWidth: 48 Layout.preferredHeight: 48 icon: "\ue8d4" } ColumnLayout { Layout.fillWidth: true spacing: 0 Label { Layout.fillWidth: true font.bold: true color: MPalette.foreground text: "This room has been replaced and is no longer active." } Label { Layout.fillWidth: true color: MPalette.lighter text: "The conversation continues here." } } } background: Rectangle { color: MPalette.banner RippleEffect { anchors.fill: parent onClicked: { roomListForm.enteredRoom = spectralController.connection.room(room.successorId) root.close() } } } } Button { Layout.alignment: Qt.AlignRight text: "Save" highlighted: true onClicked: { if (room.name != roomNameField.text) { room.setName(roomNameField.text) } if (room.topic != roomTopicField.text) { room.setTopic(roomTopicField.text) } } } MenuSeparator { Layout.fillWidth: true } ColumnLayout { Layout.fillWidth: true RowLayout { Layout.fillWidth: true Label { Layout.preferredWidth: 100 wrapMode: Label.Wrap text: "Main Alias" color: MPalette.lighter } ComboBox { Layout.fillWidth: true id: canonicalAliasComboBox model: room.remoteAliases currentIndex: room.remoteAliases.indexOf(room.canonicalAlias) onCurrentIndexChanged: { if (room.canonicalAlias != room.remoteAliases[currentIndex]) { room.setCanonicalAlias(room.remoteAliases[currentIndex]) } } } } RowLayout { Layout.fillWidth: true Label { Layout.preferredWidth: 100 Layout.alignment: Qt.AlignTop wrapMode: Label.Wrap text: "Local Aliases" color: MPalette.lighter } ColumnLayout { Layout.fillWidth: true spacing: 0 Repeater { model: room.localAliases delegate: RowLayout { Layout.maximumWidth: parent.width Label { text: modelData font.pixelSize: 12 color: MPalette.lighter } MaterialIcon { icon: "\ue5cd" color: MPalette.lighter font.pixelSize: 12 RippleEffect { anchors.fill: parent circular: true onClicked: room.removeLocalAlias(modelData) } } } } } } } } Component { id: openFileDialog OpenFileDialog {} } onClosed: destroy() } spectral/imports/Spectral/Dialog/OpenFolderDialog.qml0000644000175000000620000000026413566674120022725 0ustar dilingerstaffimport QtQuick 2.12 import Qt.labs.platform 1.1 FolderDialog { signal chosen(string path) id: root title: "Please choose a folder" onAccepted: chosen(folder) } spectral/imports/Spectral/Dialog/OpenFileDialog.qml0000644000175000000620000000025613566674120022372 0ustar dilingerstaffimport QtQuick 2.12 import Qt.labs.platform 1.1 FileDialog { signal chosen(string path) id: root title: "Please choose a file" onAccepted: chosen(file) } spectral/imports/Spectral/Dialog/UserDetailDialog.qml0000644000175000000620000001075013566674120022732 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import Spectral.Component 2.0 import Spectral.Effect 2.0 import Spectral.Setting 0.1 Dialog { property var room property var user property string displayName: user.displayName property string avatarMediaId: user.avatarMediaId property string avatarUrl: user.avatarUrl anchors.centerIn: parent width: 360 id: root modal: true contentItem: ColumnLayout { RowLayout { Layout.fillWidth: true spacing: 16 Avatar { Layout.preferredWidth: 72 Layout.preferredHeight: 72 hint: displayName source: avatarMediaId RippleEffect { anchors.fill: parent circular: true onPrimaryClicked: { if (avatarMediaId) { fullScreenImage.createObject(parent, {"filename": displayName, "localPath": room.urlToMxcUrl(avatarUrl)}).showFullScreen() } } } } ColumnLayout { Layout.fillWidth: true Label { Layout.fillWidth: true font.pixelSize: 18 font.bold: true elide: Text.ElideRight wrapMode: Text.NoWrap text: displayName color: MPalette.foreground } Label { Layout.fillWidth: true text: "Online" color: MPalette.lighter } } } MenuSeparator { Layout.fillWidth: true } RowLayout { Layout.fillWidth: true spacing: 8 MaterialIcon { Layout.preferredWidth: 32 Layout.preferredHeight: 32 Layout.alignment: Qt.AlignTop icon: "\ue88f" color: MPalette.lighter } ColumnLayout { Layout.fillWidth: true Label { Layout.fillWidth: true elide: Text.ElideRight wrapMode: Text.NoWrap text: user.id color: MPalette.accent } Label { Layout.fillWidth: true wrapMode: Label.Wrap text: "User ID" color: MPalette.lighter } } } MenuSeparator { Layout.fillWidth: true } Control { Layout.fillWidth: true contentItem: RowLayout { MaterialIcon { Layout.preferredWidth: 32 Layout.preferredHeight: 32 Layout.alignment: Qt.AlignTop icon: room.connection.isIgnored(user) ? "\ue7f5" : "\ue7f6" color: MPalette.lighter } Label { Layout.fillWidth: true wrapMode: Label.Wrap text: room.connection.isIgnored(user) ? "Unignore this user" : "Ignore this user" color: MPalette.accent } } background: RippleEffect { onPrimaryClicked: { root.close() room.connection.isIgnored(user) ? room.connection.removeFromIgnoredUsers(user) : room.connection.addToIgnoredUsers(user) } } } Control { Layout.fillWidth: true contentItem: RowLayout { MaterialIcon { Layout.preferredWidth: 32 Layout.preferredHeight: 32 Layout.alignment: Qt.AlignTop icon: "\ue5d9" color: MPalette.lighter } Label { Layout.fillWidth: true wrapMode: Label.Wrap text: "Kick this user" color: MPalette.accent } } background: RippleEffect { onPrimaryClicked: room.kickMember(user.id) } } } Component { id: fullScreenImage FullScreenImage {} } onClosed: destroy() } spectral/imports/Spectral/Dialog/JoinRoomDialog.qml0000644000175000000620000000153513566674120022426 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import Spectral.Component 2.0 Dialog { anchors.centerIn: parent width: 360 id: root title: "Start a Chat" contentItem: ColumnLayout { AutoTextField { Layout.fillWidth: true id: identifierField placeholderText: "Room Alias/User ID" } } standardButtons: Dialog.Ok | Dialog.Cancel onAccepted: { var identifier = identifierField.text var firstChar = identifier.charAt(0) if (firstChar == "@") { spectralController.createDirectChat(spectralController.connection, identifier) } else if (firstChar == "!" || firstChar == "#") { spectralController.joinRoom(spectralController.connection, identifier) } } onClosed: destroy() } spectral/imports/Spectral/Dialog/AccountDetailDialog.qml0000644000175000000620000001725413566674120023416 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import Spectral.Component 2.0 import Spectral.Effect 2.0 import Spectral 0.1 import Spectral.Setting 0.1 Dialog { anchors.centerIn: parent width: 480 id: root contentItem: Column { id: detailColumn spacing: 0 ListView { width: parent.width height: 48 clip: true orientation: ListView.Horizontal spacing: 16 model: AccountListModel{ controller: spectralController } delegate: Avatar { width: 48 height: 48 source: user.avatarMediaId hint: user.displayName || "No Name" Menu { id: contextMenu MenuItem { text: "Mark all as read" onClicked: spectralController.markAllMessagesAsRead(connection) } MenuItem { text: "Logout" onClicked: spectralController.logout(connection) } } RippleEffect { anchors.fill: parent circular: true onPrimaryClicked: spectralController.connection = connection onSecondaryClicked: contextMenu.popup() } } } RowLayout { width: parent.width MenuSeparator { Layout.fillWidth: true } ToolButton { Layout.preferredWidth: 48 Layout.preferredHeight: 48 contentItem: MaterialIcon { icon: "\ue145" color: MPalette.lighter } onClicked: loginDialog.createObject(ApplicationWindow.overlay).open() } } Control { width: parent.width contentItem: RowLayout { MaterialIcon { Layout.preferredWidth: 48 Layout.preferredHeight: 48 color: MPalette.foreground icon: "\ue7ff" } Label { Layout.fillWidth: true color: MPalette.foreground text: "Start a Chat" } } RippleEffect { anchors.fill: parent onPrimaryClicked: joinRoomDialog.createObject(ApplicationWindow.overlay).open() } } Control { width: parent.width contentItem: RowLayout { MaterialIcon { Layout.preferredWidth: 48 Layout.preferredHeight: 48 color: MPalette.foreground icon: "\ue7fc" } Label { Layout.fillWidth: true color: MPalette.foreground text: "Create a Room" } } RippleEffect { anchors.fill: parent onPrimaryClicked: createRoomDialog.createObject(ApplicationWindow.overlay).open() } } MenuSeparator { width: parent.width } Control { width: parent.width contentItem: RowLayout { MaterialIcon { Layout.preferredWidth: 48 Layout.preferredHeight: 48 color: MPalette.foreground icon: "\ue3a9" } Label { Layout.fillWidth: true color: MPalette.foreground text: "Night Mode" } Switch { id: darkThemeSwitch checked: MSettings.darkTheme onCheckedChanged: MSettings.darkTheme = checked } } RippleEffect { anchors.fill: parent onPrimaryClicked: darkThemeSwitch.checked = !darkThemeSwitch.checked } } Control { width: parent.width contentItem: RowLayout { MaterialIcon { Layout.preferredWidth: 48 Layout.preferredHeight: 48 color: MPalette.foreground icon: "\ue8f8" } Label { Layout.fillWidth: true color: MPalette.foreground text: "Show Join/Leave" } Switch { id: showJoinLeaveSwitch checked: MSettings.value("UI/show_joinleave", true) onCheckedChanged: MSettings.setValue("UI/show_joinleave", checked) } } RippleEffect { anchors.fill: parent onPrimaryClicked: showJoinLeaveSwitch.checked = !showJoinLeaveSwitch.checked } } Control { width: parent.width contentItem: RowLayout { MaterialIcon { Layout.preferredWidth: 48 Layout.preferredHeight: 48 color: MPalette.foreground icon: "\ue5d2" } Label { Layout.fillWidth: true color: MPalette.foreground text: "Enable System Tray" } Switch { id: trayIconSwitch checked: MSettings.showTray onCheckedChanged: MSettings.showTray = checked } } RippleEffect { anchors.fill: parent onPrimaryClicked: trayIconSwitch.checked = !trayIconSwitch.checked } } Control { width: parent.width contentItem: RowLayout { MaterialIcon { Layout.preferredWidth: 48 Layout.preferredHeight: 48 color: MPalette.foreground icon: "\ue7f5" } Label { Layout.fillWidth: true color: MPalette.foreground text: "Enable Notifications" } Switch { id: notificationsSwitch checked: MSettings.showNotification onCheckedChanged: MSettings.showNotification = checked } } RippleEffect { anchors.fill: parent onPrimaryClicked: notificationsSwitch.checked = !notificationsSwitch.checked } } MenuSeparator { width: parent.width } Control { width: parent.width contentItem: RowLayout { MaterialIcon { Layout.preferredWidth: 48 Layout.preferredHeight: 48 color: MPalette.foreground icon: "\ue167" } Label { Layout.fillWidth: true color: MPalette.foreground text: "Font Family" } } RippleEffect { anchors.fill: parent onPrimaryClicked: fontFamilyDialog.createObject(ApplicationWindow.overlay).open() } } } onClosed: destroy() } spectral/imports/Spectral/Dialog/qmldir0000644000175000000620000000111313566674120020242 0ustar dilingerstaffmodule Spectral.Dialog RoomSettingsDialog 2.0 RoomSettingsDialog.qml UserDetailDialog 2.0 UserDetailDialog.qml MessageSourceDialog 2.0 MessageSourceDialog.qml LoginDialog 2.0 LoginDialog.qml CreateRoomDialog 2.0 CreateRoomDialog.qml JoinRoomDialog 2.0 JoinRoomDialog.qml InviteUserDialog 2.0 InviteUserDialog.qml AcceptInvitationDialog 2.0 AcceptInvitationDialog.qml FontFamilyDialog 2.0 FontFamilyDialog.qml AccountDetailDialog 2.0 AccountDetailDialog.qml OpenFileDialog 2.0 OpenFileDialog.qml OpenFolderDialog 2.0 OpenFolderDialog.qml ImageClipboardDialog 2.0 ImageClipboardDialog.qml spectral/imports/Spectral/Dialog/MessageSourceDialog.qml0000644000175000000620000000060513566674120023434 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 Popup { property string sourceText anchors.centerIn: parent width: 480 id: root modal: true padding: 16 closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside contentItem: ScrollView { clip: true Label { text: sourceText } } onClosed: destroy() } spectral/imports/Spectral/Dialog/AcceptInvitationDialog.qml0000644000175000000620000000142613566674120024135 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 Dialog { property var room anchors.centerIn: parent width: 360 id: root title: "Invitation Received" modal: true contentItem: Label { text: "Accept this invitation?" } footer: DialogButtonBox { Button { text: "Accept" flat: true onClicked: { room.acceptInvitation() close() } } Button { text: "Reject" flat: true onClicked: { room.forget() close() } } Button { text: "Cancel" flat: true onClicked: close() } } onClosed: destroy() } spectral/imports/Spectral/Dialog/CreateRoomDialog.qml0000644000175000000620000000132413566674120022726 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import Spectral.Component 2.0 Dialog { anchors.centerIn: parent width: 360 id: root title: "Create a Room" contentItem: ColumnLayout { AutoTextField { Layout.fillWidth: true id: roomNameField placeholderText: "Room Name" } AutoTextField { Layout.fillWidth: true id: roomTopicField placeholderText: "Room Topic" } } standardButtons: Dialog.Ok | Dialog.Cancel onAccepted: spectralController.createRoom(spectralController.connection, roomNameField.text, roomTopicField.text) onClosed: destroy() } spectral/imports/Spectral/Panel/0002755000175000000620000000000013566674120016675 5ustar dilingerstaffspectral/imports/Spectral/Panel/RoomPanelInput.qml0000644000175000000620000004065113566674120022330 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import Spectral.Component 2.0 import Spectral.Component.Emoji 2.0 import Spectral.Dialog 2.0 import Spectral.Effect 2.0 import Spectral.Setting 0.1 import Spectral 0.1 Control { property alias isReply: replyItem.visible property bool isReaction: false property var replyModel readonly property var replyUser: replyModel ? replyModel.author : null readonly property string replyEventID: replyModel ? replyModel.eventId : "" readonly property string replyContent: replyModel ? replyModel.display : "" property alias isAutoCompleting: autoCompleteListView.visible property var autoCompleteModel property int autoCompleteBeginPosition property int autoCompleteEndPosition property bool hasAttachment: false property url attachmentPath id: root padding: 0 background: Rectangle { color: MSettings.darkTheme ? "#303030" : "#fafafa" radius: 24 layer.enabled: true layer.effect: ElevationEffect { elevation: 1 } } contentItem: ColumnLayout { spacing: 0 RowLayout { Layout.fillWidth: true Layout.margins: 8 id: replyItem visible: false spacing: 8 Control { Layout.alignment: Qt.AlignTop padding: 4 background: Rectangle { radius: height / 2 color: replyUser ? Qt.darker(replyUser.color, 1.1) : MPalette.accent } contentItem: RowLayout { Avatar { Layout.preferredWidth: 24 Layout.preferredHeight: 24 source: replyUser ? replyUser.avatarMediaId : "" hint: replyUser ? replyUser.displayName : "No name" } Label { Layout.alignment: Qt.AlignVCenter text: replyUser ? replyUser.displayName : "No name" color: "white" font.weight: Font.Medium rightPadding: 8 } } } TextEdit { Layout.fillWidth: true color: MPalette.foreground text: "" + replyContent font.family: window.font.family font.pixelSize: 14 selectByMouse: true readOnly: true wrapMode: Label.Wrap selectedTextColor: "white" selectionColor: MPalette.accent textFormat: Text.RichText } } EmojiPicker { Layout.fillWidth: true id: emojiPicker visible: false textArea: inputField emojiModel: EmojiModel { id: emojiModel } } ListView { Layout.fillWidth: true Layout.preferredHeight: 36 Layout.margins: 8 id: autoCompleteListView visible: false model: autoCompleteModel clip: true spacing: 4 orientation: ListView.Horizontal highlightFollowsCurrentItem: true keyNavigationWraps: true delegate: Control { property string autoCompleteText: modelData.displayName || modelData.unicode property bool isEmoji: modelData.unicode != null readonly property bool highlighted: autoCompleteListView.currentIndex === index height: 36 padding: 6 background: Rectangle { visible: !isEmoji color: highlighted ? border.color : "transparent" border.color: isEmoji ? Material.accent : modelData.color border.width: 2 radius: height / 2 } contentItem: RowLayout { spacing: 6 Text { width: 24 height: 24 visible: isEmoji text: autoCompleteText font.pixelSize: 24 font.family: "Emoji" verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } Avatar { Layout.preferredWidth: 24 Layout.preferredHeight: 24 visible: !isEmoji source: modelData.avatarMediaId || null color: modelData.color ? Qt.darker(modelData.color, 1.1) : MPalette.accent } Label { Layout.fillHeight: true visible: !isEmoji text: autoCompleteText color: highlighted ? "white" : MPalette.foreground verticalAlignment: Text.AlignVCenter rightPadding: 8 } } MouseArea { anchors.fill: parent onClicked: { autoCompleteListView.currentIndex = index inputField.replaceAutoComplete(autoCompleteText) } } } } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 1 Layout.leftMargin: 12 Layout.rightMargin: 12 visible: emojiPicker.visible || replyItem.visible || autoCompleteListView.visible color: MSettings.darkTheme ? "#424242" : "#e7ebeb" } RowLayout { Layout.fillWidth: true spacing: 0 ToolButton { Layout.preferredWidth: 48 Layout.preferredHeight: 48 Layout.alignment: Qt.AlignBottom id: uploadButton visible: !isReply && !hasAttachment contentItem: MaterialIcon { icon: "\ue226" } onClicked: { if (imageClipboard.hasImage) { attachDialog.open() } else { var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay) fileDialog.chosen.connect(function(path) { if (!path) return roomPanelInput.attach(path) }) fileDialog.open() } } BusyIndicator { anchors.fill: parent running: currentRoom && currentRoom.hasFileUploading } } ToolButton { Layout.preferredWidth: 48 Layout.preferredHeight: 48 Layout.alignment: Qt.AlignBottom id: cancelReplyButton visible: isReply contentItem: MaterialIcon { icon: "\ue5cd" } onClicked: clearReply() } Control { Layout.margins: 6 Layout.preferredHeight: 36 Layout.alignment: Qt.AlignVCenter visible: hasAttachment rightPadding: 8 background: Rectangle { color: MPalette.accent radius: height / 2 antialiasing: true } contentItem: RowLayout { spacing: 0 ToolButton { Layout.preferredWidth: height Layout.fillHeight: true id: cancelAttachmentButton contentItem: MaterialIcon { icon: "\ue5cd" color: "white" font.pixelSize: 18 } onClicked: { hasAttachment = false attachmentPath = "" } } Label { Layout.alignment: Qt.AlignVCenter text: attachmentPath != "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : "" color: "white" } } } TextArea { property real progress: 0 Layout.fillWidth: true Layout.minimumHeight: 48 id: inputField wrapMode: Text.Wrap placeholderText: "Send a Message" topPadding: 0 bottomPadding: 0 selectByMouse: true verticalAlignment: TextEdit.AlignVCenter text: currentRoom != null ? currentRoom.cachedInput : "" background: Item {} Rectangle { width: currentRoom && currentRoom.hasFileUploading ? parent.width * currentRoom.fileUploadingProgress / 100 : 0 height: parent.height opacity: 0.2 color: Material.accent } Timer { id: timeoutTimer repeat: false interval: 2000 onTriggered: { repeatTimer.stop() currentRoom.sendTypingNotification(false) } } Timer { id: repeatTimer repeat: true interval: 5000 triggeredOnStart: true onTriggered: currentRoom.sendTypingNotification(true) } Keys.onReturnPressed: { if (event.modifiers & Qt.ShiftModifier) { insert(cursorPosition, "\n") } else { postMessage(text) text = "" clearReply() closeAll() } } Keys.onEscapePressed: closeAll() Keys.onBacktabPressed: if (isAutoCompleting) autoCompleteListView.decrementCurrentIndex() Keys.onTabPressed: { if (isAutoCompleting) { autoCompleteListView.incrementCurrentIndex() } else { autoCompleteBeginPosition = text.substring(0, cursorPosition).lastIndexOf(" ") + 1 var autoCompletePrefix = text.substring(0, cursorPosition).split(" ").pop() if (!autoCompletePrefix) return if (autoCompletePrefix.startsWith(":")) { autoCompleteBeginPosition = text.substring(0, cursorPosition).lastIndexOf(" ") + 1 autoCompleteModel = emojiModel.filterModel(autoCompletePrefix) if (autoCompleteModel.length === 0) return isAutoCompleting = true autoCompleteEndPosition = cursorPosition } else { autoCompleteModel = currentRoom.getUsers(autoCompletePrefix) if (autoCompleteModel.length === 0) return isAutoCompleting = true autoCompleteEndPosition = cursorPosition } } replaceAutoComplete(autoCompleteListView.currentItem.autoCompleteText) } onTextChanged: { timeoutTimer.restart() repeatTimer.start() currentRoom.cachedInput = text if (cursorPosition !== autoCompleteBeginPosition && cursorPosition !== autoCompleteEndPosition) { isAutoCompleting = false autoCompleteListView.currentIndex = 0 } } function replaceAutoComplete(word) { remove(autoCompleteBeginPosition, autoCompleteEndPosition) autoCompleteEndPosition = autoCompleteBeginPosition + word.length insert(cursorPosition, word) } function postMessage(text) { if(!currentRoom) { return } if (hasAttachment) { currentRoom.uploadFile(attachmentPath, text) clearAttachment() return } if (text.trim().length === 0) { return } var PREFIX_ME = '/me ' var PREFIX_NOTICE = '/notice ' var PREFIX_RAINBOW = '/rainbow ' var messageEventType = RoomMessageEvent.Text if (text.indexOf(PREFIX_RAINBOW) === 0) { text = text.substr(PREFIX_RAINBOW.length) var parsedText = "" var rainbowColor = ["#ff2b00", "#ff5500", "#ff8000", "#ffaa00", "#ffd500", "#ffff00", "#d4ff00", "#aaff00", "#80ff00", "#55ff00", "#2bff00", "#00ff00", "#00ff2b", "#00ff55", "#00ff80", "#00ffaa", "#00ffd5", "#00ffff", "#00d4ff", "#00aaff", "#007fff", "#0055ff", "#002bff", "#0000ff", "#2a00ff", "#5500ff", "#7f00ff", "#aa00ff", "#d400ff", "#ff00ff", "#ff00d4", "#ff00aa", "#ff0080", "#ff0055", "#ff002b", "#ff0000"] for (var i = 0; i < text.length; i++) { parsedText = parsedText + "" + text.charAt(i) + "" } currentRoom.postHtmlMessage(text, parsedText, RoomMessageEvent.Text, replyEventID) return } if (text.indexOf(PREFIX_ME) === 0) { text = text.substr(PREFIX_ME.length) messageEventType = RoomMessageEvent.Emote } else if (text.indexOf(PREFIX_NOTICE) === 0) { text = text.substr(PREFIX_NOTICE.length) messageEventType = RoomMessageEvent.Notice } if (MSettings.markdownFormatting) { currentRoom.postArbitaryMessage(text, messageEventType, replyEventID) } else { currentRoom.postPlainMessage(text, messageEventType, replyEventID) } } } MaterialIcon { Layout.alignment: Qt.AlignVCenter icon: "\ue165" font.pixelSize: 16 color: MPalette.foreground opacity: MSettings.markdownFormatting ? 1 : 0.3 MouseArea { anchors.fill: parent onClicked: MSettings.markdownFormatting = !MSettings.markdownFormatting } } ToolButton { Layout.preferredWidth: 48 Layout.preferredHeight: 48 Layout.alignment: Qt.AlignBottom id: emojiButton contentItem: MaterialIcon { icon: "\ue24e" } onClicked: emojiPicker.visible = !emojiPicker.visible } } } function insert(str) { inputField.insert(inputField.cursorPosition, str) } function clear() { inputField.clear() } function clearReply() { isReply = false replyModel = null } function focus() { inputField.forceActiveFocus() } function closeAll() { replyItem.visible = false autoCompleteListView.visible = false emojiPicker.visible = false } function attach(localPath) { hasAttachment = true attachmentPath = localPath } function clearAttachment() { hasAttachment = false attachmentPath = "" } } spectral/imports/Spectral/Panel/RoomListPanel.qml0000644000175000000620000002761213566674120022146 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import Spectral.Component 2.0 import Spectral.Dialog 2.0 import Spectral.Menu 2.0 import Spectral.Effect 2.0 import Spectral 0.1 import Spectral.Setting 0.1 import SortFilterProxyModel 0.2 Item { property var connection: null readonly property var user: connection ? connection.localUser : null property int filter: 0 property var enteredRoom: null signal enterRoom(var room) signal leaveRoom(var room) id: root RoomListModel { id: roomListModel connection: root.connection onNewMessage: if (!window.active && MSettings.showNotification) notificationsManager.postNotification(roomId, eventId, roomName, senderName, text, icon) } Binding { target: trayIcon property: "notificationCount" value: roomListModel.notificationCount } SortFilterProxyModel { id: sortedRoomListModel sourceModel: roomListModel proxyRoles: ExpressionRole { name: "display" expression: { switch (category) { case 1: return "Invited" case 2: return "Favorites" case 3: return "People" case 4: return "Rooms" case 5: return "Low Priority" } } } sorters: [ RoleSorter { roleName: "category" }, ExpressionSorter { expression: { return modelLeft.highlightCount > 0; } }, ExpressionSorter { expression: { return modelLeft.notificationCount > 0; } }, RoleSorter { roleName: "lastActiveTime" sortOrder: Qt.DescendingOrder } ] filters: [ ExpressionFilter { expression: joinState != "upgraded" }, RegExpFilter { roleName: "name" pattern: searchField.text caseSensitivity: Qt.CaseInsensitive }, ExpressionFilter { enabled: filter === 0 expression: category !== 5 && notificationCount > 0 || currentRoom === enteredRoom }, ExpressionFilter { enabled: filter === 1 expression: category === 1 || category === 3 }, ExpressionFilter { enabled: filter === 2 expression: category !== 3 } ] } Shortcut { sequence: "Ctrl+F" onActivated: searchField.forceActiveFocus() } ColumnLayout { anchors.fill: parent spacing: 0 Control { Layout.fillWidth: true Layout.preferredHeight: 64 id: roomListHeader topPadding: 12 bottomPadding: 12 leftPadding: 12 rightPadding: 18 contentItem: RowLayout { ToolButton { Layout.preferredWidth: height Layout.fillHeight: true visible: !searchField.active contentItem: MaterialIcon { icon: "\ue8b6" } } ToolButton { Layout.preferredWidth: height Layout.fillHeight: true visible: searchField.active contentItem: MaterialIcon { icon: "\ue5cd" } onClicked: searchField.clear() } AutoTextField { readonly property bool active: text Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter id: searchField placeholderText: "Search..." color: MPalette.lighter } Avatar { Layout.preferredWidth: height Layout.fillHeight: true Layout.alignment: Qt.AlignRight visible: !searchField.active source: root.user ? root.user.avatarMediaId : null hint: root.user ? root.user.displayName : "?" RippleEffect { anchors.fill: parent circular: true onClicked: accountDetailDialog.createObject(ApplicationWindow.overlay).open() } } } background: Rectangle { color: Material.background layer.enabled: true layer.effect: ElevationEffect { elevation: 2 } } } AutoListView { Layout.fillWidth: true Layout.fillHeight: true id: listView z: -1 spacing: 0 model: sortedRoomListModel boundsBehavior: Flickable.DragOverBounds ScrollBar.vertical: ScrollBar {} delegate: Item { width: listView.width height: 64 Rectangle { anchors.fill: parent visible: currentRoom === enteredRoom color: Material.accent opacity: 0.1 } RowLayout { anchors.fill: parent anchors.margins: 12 spacing: 12 Avatar { Layout.preferredWidth: height Layout.fillHeight: true source: avatar hint: name || "No Name" } ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true Layout.alignment: Qt.AlignHCenter Label { Layout.fillWidth: true Layout.fillHeight: true text: name || "No Name" color: MPalette.foreground font.pixelSize: 16 font.bold: unreadCount >= 0 elide: Text.ElideRight wrapMode: Text.NoWrap } Label { Layout.fillWidth: true Layout.fillHeight: true text: (lastEvent == "" ? topic : lastEvent).replace(/(\r\n\t|\n|\r\t)/gm," ") color: MPalette.lighter font.pixelSize: 13 elide: Text.ElideRight wrapMode: Text.NoWrap } } Label { visible: notificationCount > 0 && highlightCount == 0 color: MPalette.background text: notificationCount leftPadding: 12 rightPadding: 12 topPadding: 4 bottomPadding: 4 font.bold: true background: Rectangle { radius: height / 2 color: MPalette.lighter } } Label { visible: highlightCount > 0 color: "white" text: highlightCount leftPadding: 12 rightPadding: 12 topPadding: 4 bottomPadding: 4 font.bold: true background: Rectangle { radius: height / 2 color: MPalette.accent } } } AutoMouseArea { anchors.fill: parent onPrimaryClicked: { if (category === RoomType.Invited) { acceptInvitationDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom}).open() } else { if (enteredRoom) { leaveRoom(enteredRoom) enteredRoom.displayed = false } enterRoom(currentRoom) enteredRoom = currentRoom currentRoom.displayed = true } } onSecondaryClicked: roomListContextMenu.createObject(parent, {"room": currentRoom}).popup() } Component { id: roomListContextMenu RoomListContextMenu {} } } section.property: "display" section.criteria: ViewSection.FullString section.delegate: Label { width: parent.width height: 24 text: section color: MPalette.lighter leftPadding: 16 elide: Text.ElideRight verticalAlignment: Text.AlignVCenter } ColumnLayout { anchors.centerIn: parent visible: sortedRoomListModel.count == 0 MaterialIcon { Layout.alignment: Qt.AlignHCenter icon: "\ue5ca" font.pixelSize: 48 color: MPalette.lighter } Label { Layout.alignment: Qt.AlignHCenter text: "You're all caught up!" color: MPalette.foreground } } } Control { Layout.fillWidth: true Layout.preferredHeight: 48 Layout.margins: 16 padding: 8 contentItem: RowLayout { id: tabBar MaterialIcon { Layout.fillWidth: true icon: "\ue7f5" color: filter == 0 ? MPalette.accent : MPalette.lighter MouseArea { anchors.fill: parent onClicked: filter = 0 } } MaterialIcon { Layout.fillWidth: true icon: "\ue7ff" color: filter == 1 ? MPalette.accent : MPalette.lighter MouseArea { anchors.fill: parent onClicked: filter = 1 } } MaterialIcon { Layout.fillWidth: true icon: "\ue7fc" color: filter == 2 ? MPalette.accent : MPalette.lighter MouseArea { anchors.fill: parent onClicked: filter = 2 } } } background: AutoRectangle { color: MPalette.background radius: 24 topLeftRadius: 8 topRightRadius: 8 topLeftVisible: true topRightVisible: true bottomLeftVisible: false bottomRightVisible: false layer.enabled: true layer.effect: ElevationEffect { elevation: 1 } } } } Component { id: acceptInvitationDialog AcceptInvitationDialog {} } } spectral/imports/Spectral/Panel/RoomDrawer.qml0000644000175000000620000001547313566674120021501 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Controls.Material 2.12 import QtQuick.Layouts 1.12 import Spectral.Component 2.0 import Spectral.Dialog 2.0 import Spectral.Effect 2.0 import Spectral.Setting 0.1 import Spectral 0.1 Drawer { property var room id: roomDrawer edge: Qt.RightEdge ColumnLayout { anchors.fill: parent anchors.margins: 24 Component { id: fullScreenImage FullScreenImage {} } RowLayout { Layout.fillWidth: true spacing: 16 Avatar { Layout.preferredWidth: 72 Layout.preferredHeight: 72 hint: room ? room.displayName : "No name" source: room ? room.avatarMediaId : null RippleEffect { anchors.fill: parent circular: true onClicked: fullScreenImage.createObject(parent, {"filename": room.diaplayName, "localPath": room.urlToMxcUrl(room.avatarUrl)}).showFullScreen() } } ColumnLayout { Layout.fillWidth: true Label { Layout.fillWidth: true font.pixelSize: 18 font.bold: true wrapMode: Label.Wrap text: room ? room.displayName : "No Name" color: MPalette.foreground } Label { Layout.fillWidth: true wrapMode: Label.Wrap text: room ? room.totalMemberCount + " Members" : "No Member Count" color: MPalette.lighter } } } MenuSeparator { Layout.fillWidth: true } Control { Layout.fillWidth: true padding: 0 contentItem: ColumnLayout { RowLayout { Layout.fillWidth: true visible: room && room.canonicalAlias spacing: 8 MaterialIcon { Layout.preferredWidth: 32 Layout.preferredHeight: 32 Layout.alignment: Qt.AlignTop icon: "\ue2bc" color: MPalette.lighter } ColumnLayout { Layout.fillWidth: true Label { Layout.fillWidth: true wrapMode: Label.Wrap text: room && room.canonicalAlias ? room.canonicalAlias : "No Canonical Alias" color: MPalette.accent } Label { Layout.fillWidth: true wrapMode: Label.Wrap text: "Main Alias" color: MPalette.lighter } } } RowLayout { Layout.fillWidth: true spacing: 8 MaterialIcon { Layout.preferredWidth: 32 Layout.preferredHeight: 32 Layout.alignment: Qt.AlignTop icon: "\ue88f" color: MPalette.lighter } ColumnLayout { Layout.fillWidth: true Label { Layout.fillWidth: true wrapMode: Label.Wrap text: room && room.topic ? room.topic : "No Topic" color: MPalette.foreground maximumLineCount: 5 elide: Text.ElideRight } Label { Layout.fillWidth: true wrapMode: Label.Wrap text: "Topic" color: MPalette.lighter } } } } background: AutoMouseArea { onPrimaryClicked: roomSettingDialog.createObject(ApplicationWindow.overlay, {"room": room}).open() } } MenuSeparator { Layout.fillWidth: true } RowLayout { Layout.fillWidth: true spacing: 8 MaterialIcon { Layout.preferredWidth: 32 Layout.preferredHeight: 32 icon: "\ue7ff" color: MPalette.lighter } Label { Layout.fillWidth: true wrapMode: Label.Wrap text: room ? room.totalMemberCount + " Members" : "No Member Count" color: MPalette.lighter } ToolButton { Layout.preferredWidth: 32 Layout.preferredHeight: 32 contentItem: MaterialIcon { icon: "\ue145" color: MPalette.lighter } onClicked: inviteUserDialog.createObject(ApplicationWindow.overlay, {"room": room}).open() } } AutoListView { Layout.fillWidth: true Layout.fillHeight: true id: userListView clip: true boundsBehavior: Flickable.DragOverBounds model: UserListModel { room: roomDrawer.room } delegate: Item { width: userListView.width height: 48 RowLayout { anchors.fill: parent anchors.margins: 8 spacing: 12 Avatar { Layout.preferredWidth: height Layout.fillHeight: true source: avatar hint: name } Label { Layout.fillWidth: true text: name color: MPalette.foreground } } RippleEffect { anchors.fill: parent onPrimaryClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": room, "user": user}).open() } } ScrollBar.vertical: ScrollBar {} } } Component { id: roomSettingDialog RoomSettingsDialog {} } Component { id: userDetailDialog UserDetailDialog {} } Component { id: inviteUserDialog InviteUserDialog {} } } spectral/imports/Spectral/Panel/RoomHeader.qml0000644000175000000620000000215413566674120021435 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import Spectral 0.1 import Spectral.Effect 2.0 import Spectral.Component 2.0 import Spectral.Setting 0.1 Control { signal clicked() id: header background: Rectangle { color: MPalette.background layer.enabled: true layer.effect: ElevationEffect { elevation: 2 } } RowLayout { anchors.fill: parent anchors.leftMargin: 18 Layout.alignment: Qt.AlignVCenter spacing: 12 Label { Layout.fillWidth: true text: currentRoom ? currentRoom.displayName : "" color: MPalette.foreground font.pixelSize: 18 elide: Text.ElideRight wrapMode: Text.NoWrap } ToolButton { Layout.preferredWidth: height Layout.fillHeight: true contentItem: MaterialIcon { icon: "\ue5d4" color: MPalette.lighter } onClicked: header.clicked() } } } spectral/imports/Spectral/Panel/qmldir0000644000175000000620000000016413566674120020107 0ustar dilingerstaffmodule Spectral.Panel RoomPanel 2.0 RoomPanel.qml RoomListPanel 2.0 RoomListPanel.qml RoomDrawer 2.0 RoomDrawer.qml spectral/imports/Spectral/Panel/RoomPanel.qml0000644000175000000620000004770413566674120021316 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import Qt.labs.qmlmodels 1.0 import Qt.labs.platform 1.0 import QtGraphicalEffects 1.0 import Spectral.Component 2.0 import Spectral.Component.Emoji 2.0 import Spectral.Component.Timeline 2.0 import Spectral.Dialog 2.0 import Spectral.Effect 2.0 import Spectral 0.1 import Spectral.Setting 0.1 import SortFilterProxyModel 0.2 Item { property var currentRoom: null id: root MessageEventModel { id: messageEventModel room: currentRoom } DropArea { anchors.fill: parent enabled: currentRoom onDropped: { if (!drop.hasUrls) return roomPanelInput.attach(drop.urls[0]) } } ImageClipboard { id: imageClipboard } Popup { anchors.centerIn: parent id: attachDialog padding: 16 contentItem: RowLayout { Control { Layout.preferredWidth: 160 Layout.fillHeight: true padding: 16 contentItem: ColumnLayout { spacing: 16 MaterialIcon { Layout.alignment: Qt.AlignHCenter icon: "\ue2c8" font.pixelSize: 64 color: MPalette.lighter } Label { Layout.alignment: Qt.AlignHCenter text: "Choose local file" color: MPalette.foreground } } background: RippleEffect { onClicked: { attachDialog.close() var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay) fileDialog.chosen.connect(function(path) { if (!path) return roomPanelInput.attach(path) }) fileDialog.open() } } } Rectangle { Layout.preferredWidth: 1 Layout.fillHeight: true color: MPalette.banner } Control { Layout.preferredWidth: 160 Layout.fillHeight: true padding: 16 contentItem: ColumnLayout { spacing: 16 MaterialIcon { Layout.alignment: Qt.AlignHCenter icon: "\ue410" font.pixelSize: 64 color: MPalette.lighter } Label { Layout.alignment: Qt.AlignHCenter text: "Clipboard image" color: MPalette.foreground } } background: RippleEffect { onClicked: { var localPath = StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png" if (!imageClipboard.saveImage(localPath)) return roomPanelInput.attach(localPath) attachDialog.close() } } } } } Component { id: openFileDialog OpenFileDialog {} } Column { anchors.centerIn: parent spacing: 16 visible: !currentRoom Image { anchors.horizontalCenter: parent.horizontalCenter width: 240 fillMode: Image.PreserveAspectFit source: "qrc:/assets/img/matrix.svg" } Label { anchors.horizontalCenter: parent.horizontalCenter text: "Welcome to Matrix, a new era of instant messaging." } Label { anchors.horizontalCenter: parent.horizontalCenter text: "To start chatting, select a room from the room list." } } Rectangle { anchors.fill: parent visible: currentRoom color: MSettings.darkTheme ? "#242424" : "#EBEFF2" } ColumnLayout { anchors.fill: parent spacing: 0 visible: currentRoom RoomHeader { Layout.fillWidth: true Layout.preferredHeight: 64 z: 10 id: roomHeader onClicked: roomDrawer.visible ? roomDrawer.close() : roomDrawer.open() } ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true Layout.maximumWidth: 960 Layout.alignment: Qt.AlignHCenter Layout.leftMargin: 16 Layout.rightMargin: 16 Layout.bottomMargin: 16 width: Math.min(parent.width - 32, 960) spacing: 16 AutoListView { readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1 readonly property bool noNeedMoreContent: !currentRoom || currentRoom.eventsHistoryJob || currentRoom.allHistoryLoaded Layout.fillWidth: true Layout.fillHeight: true id: messageListView spacing: 2 displayMarginBeginning: 100 displayMarginEnd: 100 verticalLayoutDirection: ListView.BottomToTop highlightMoveDuration: 500 boundsBehavior: Flickable.DragOverBounds model: SortFilterProxyModel { id: sortedMessageEventModel sourceModel: messageEventModel filters: [ ExpressionFilter { expression: marks !== 0x10 && eventType !== "other" } ] onModelReset: { movingTimer.stop() messageListView.positionViewAtBeginning() if (currentRoom) { movingTimer.restart() // var lastScrollPosition = sortedMessageEventModel.mapFromSource(currentRoom.savedTopVisibleIndex()) // if (lastScrollPosition === 0) { // messageListView.positionViewAtBeginning() // } else { // messageListView.currentIndex = lastScrollPosition // } if (messageListView.contentY < messageListView.originY + 10 || currentRoom.timelineSize < 20) currentRoom.getPreviousContent(50) } } } onContentYChanged: { if(!noNeedMoreContent && contentY - 5000 < originY) currentRoom.getPreviousContent(20); } populate: Transition { NumberAnimation { property: "opacity"; from: 0; to: 1 duration: 200 } } add: Transition { NumberAnimation { property: "opacity"; from: 0; to: 1 duration: 200 } } move: Transition { NumberAnimation { property: "y"; duration: 200 } NumberAnimation { property: "opacity"; to: 1 } } displaced: Transition { NumberAnimation { property: "y"; duration: 200 easing.type: Easing.OutQuad } NumberAnimation { property: "opacity"; to: 1 } } delegate: DelegateChooser { role: "eventType" DelegateChoice { roleValue: "state" delegate: StateDelegate { anchors.horizontalCenter: parent.horizontalCenter } } DelegateChoice { roleValue: "emote" delegate: StateDelegate { anchors.horizontalCenter: parent.horizontalCenter } } DelegateChoice { roleValue: "message" delegate: ColumnLayout { width: messageListView.width SectionDelegate { Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width visible: showSection } MessageDelegate { Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 2 visible: readMarker color: MPalette.primary } } } DelegateChoice { roleValue: "notice" delegate: ColumnLayout { width: messageListView.width SectionDelegate { Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width visible: showSection } MessageDelegate { Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 2 visible: readMarker color: MPalette.primary } } } DelegateChoice { roleValue: "image" delegate: ColumnLayout { width: messageListView.width SectionDelegate { Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width visible: showSection } ImageDelegate { Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 2 visible: readMarker color: MPalette.primary } } } DelegateChoice { roleValue: "audio" delegate: ColumnLayout { width: messageListView.width SectionDelegate { Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width visible: showSection } AudioDelegate { Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 2 visible: readMarker color: MPalette.primary } } } DelegateChoice { roleValue: "video" delegate: ColumnLayout { width: messageListView.width SectionDelegate { Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width visible: showSection } VideoDelegate { Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 2 visible: readMarker color: MPalette.primary } } } DelegateChoice { roleValue: "file" delegate: ColumnLayout { width: messageListView.width SectionDelegate { Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width visible: showSection } FileDelegate { Layout.alignment: sentByMe ? Qt.AlignRight : Qt.AlignLeft } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 2 visible: readMarker color: MPalette.primary } } } DelegateChoice { roleValue: "other" delegate: Item {} } } Control { anchors.right: parent.right anchors.top: parent.top anchors.topMargin: 16 padding: 8 id: goReadMarkerFab visible: currentRoom && currentRoom.hasUnreadMessages contentItem: MaterialIcon { icon: "\ue316" font.pixelSize: 28 } background: Rectangle { color: MPalette.background radius: height / 2 layer.enabled: true layer.effect: ElevationEffect { elevation: 2 } RippleEffect { anchors.fill: parent circular: true onClicked: goToEvent(currentRoom.readMarkerEventId) } } } Control { anchors.right: parent.right anchors.bottom: parent.bottom padding: 8 id: goTopFab visible: !messageListView.atYEnd contentItem: MaterialIcon { icon: "\ue313" font.pixelSize: 28 } background: Rectangle { color: MPalette.background radius: height / 2 layer.enabled: true layer.effect: ElevationEffect { elevation: 2 } RippleEffect { anchors.fill: parent circular: true onClicked: { currentRoom.markAllMessagesAsRead() messageListView.positionViewAtBeginning() } } } } Control { anchors.left: parent.left anchors.bottom: parent.bottom visible: currentRoom && currentRoom.usersTyping.length > 0 padding: 4 contentItem: RowLayout { spacing: 0 RowLayout { spacing: -8 Repeater { model: currentRoom && currentRoom.usersTyping.length > 0 ? currentRoom.usersTyping : null delegate: Rectangle { Layout.preferredWidth: 28 Layout.preferredHeight: 28 color: "white" radius: 14 Avatar { anchors.fill: parent anchors.margins: 2 source: modelData.avatarMediaId hint: modelData.displayName color: modelData.color } } } } Item { Layout.preferredWidth: 28 Layout.preferredHeight: 28 BusyIndicator { anchors.centerIn: parent width: 32 height: 32 } } } background: Rectangle { color: MPalette.background radius: height / 2 layer.enabled: true layer.effect: ElevationEffect { elevation: 1 } } } Keys.onUpPressed: scrollBar.decrease() Keys.onDownPressed: scrollBar.increase() ScrollBar.vertical: ScrollBar { id: scrollBar } } RoomPanelInput { Layout.fillWidth: true id: roomPanelInput } } } Timer { id: movingTimer interval: 10000 repeat: true running: false onTriggered: saveReadMarker() } function goToEvent(eventID) { var index = messageEventModel.eventIDToIndex(eventID) if (index === -1) return messageListView.positionViewAtIndex(sortedMessageEventModel.mapFromSource(index), ListView.Contain) } function saveViewport() { currentRoom.saveViewport(sortedMessageEventModel.mapToSource(messageListView.indexAt(messageListView.contentX + (messageListView.width / 2), messageListView.contentY)), sortedMessageEventModel.mapToSource(messageListView.largestVisibleIndex)) } function saveReadMarker() { var readMarker = sortedMessageEventModel.get(messageListView.largestVisibleIndex).eventId if (!readMarker) return currentRoom.readMarkerEventId = readMarker } } spectral/imports/Spectral/Menu/0002755000175000000620000000000013566674120016542 5ustar dilingerstaffspectral/imports/Spectral/Menu/RoomListContextMenu.qml0000644000175000000620000000151213566674120023214 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Controls.Material 2.12 Menu { property var room id: root MenuItem { text: "Favourite" checkable: true checked: room.isFavourite onTriggered: room.isFavourite ? room.removeTag("m.favourite") : room.addTag("m.favourite", 1.0) } MenuItem { text: "Deprioritize" checkable: true checked: room.isLowPriority onTriggered: room.isLowPriority ? room.removeTag("m.lowpriority") : room.addTag("m.lowpriority", 1.0) } MenuSeparator {} MenuItem { text: "Mark as Read" onTriggered: room.markAllMessagesAsRead() } MenuItem { text: "Leave Room" Material.foreground: Material.Red onTriggered: room.forget() } onClosed: destroy() } spectral/imports/Spectral/Menu/Timeline/0002755000175000000620000000000013566674120020310 5ustar dilingerstaffspectral/imports/Spectral/Menu/Timeline/MessageDelegateContextMenu.qml0000644000175000000620000000234413566674120026235 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import Spectral.Dialog 2.0 Menu { readonly property string selectedText: contentLabel.selectedText signal viewSource() signal reply() signal redact() id: root Item { width: parent.width height: 32 Row { anchors.centerIn: parent spacing: 0 Repeater { model: ["👍", "👎️", "😄", "🎉", "🚀", "👀"] delegate: ItemDelegate { width: 32 height: 32 contentItem: Label { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pixelSize: 16 text: modelData } onClicked: currentRoom.toggleReaction(eventId, modelData) } } } } MenuSeparator {} MenuItem { text: "View Source" onTriggered: viewSource() } MenuItem { text: "Reply" onTriggered: reply() } MenuItem { text: "Redact" onTriggered: redact() } onClosed: destroy() } spectral/imports/Spectral/Menu/Timeline/qmldir0000644000175000000620000000022413566674120021517 0ustar dilingerstaffmodule Spectral.Menu.Timeline MessageDelegateContextMenu 2.0 MessageDelegateContextMenu.qml FileDelegateContextMenu 2.0 FileDelegateContextMenu.qml spectral/imports/Spectral/Menu/Timeline/FileDelegateContextMenu.qml0000644000175000000620000000121413566674120025523 0ustar dilingerstaffimport QtQuick 2.12 import QtQuick.Controls 2.12 import Spectral.Dialog 2.0 Menu { signal viewSource() signal downloadAndOpen() signal saveFileAs() signal reply() signal redact() id: root MenuItem { text: "View Source" onTriggered: viewSource() } MenuItem { text: "Open Externally" onTriggered: downloadAndOpen() } MenuItem { text: "Save As" onTriggered: saveFileAs() } MenuItem { text: "Reply" onTriggered: reply() } MenuItem { text: "Redact" onTriggered: redact() } onClosed: destroy() } spectral/imports/Spectral/Menu/qmldir0000644000175000000620000000010513566674120017747 0ustar dilingerstaffmodule Spectral.Menu RoomListContextMenu 2.0 RoomListContextMenu.qml spectral/include/0002755000175000000620000000000013566674120014007 5ustar dilingerstaffspectral/include/SortFilterProxyModel/0002755000175000000620000000000013566674121020130 5ustar dilingerstaffspectral/include/SortFilterProxyModel/README.md0000644000175000000620000001051613566674121021410 0ustar dilingerstaffSortFilterProxyModel ==================== SortFilterProxyModel is an implementation of `QSortFilterProxyModel` conveniently exposed for QML. Install ------- ##### With [qpm](https://qpm.io) : 1. `qpm install fr.grecko.sortfilterproxymodel` 2. add `include(vendor/vendor.pri)` in your .pro if it is not already done 3. `import SortFilterProxyModel 0.2` to use this library in your QML files ##### Without qpm : 1. clone or download this repository 2. * `qmake` add `include (/SortFilterProxyModel.pri)` in your `.pro` * `CMake` add $ to the sources of your executable target in your cmake project 3. `import SortFilterProxyModel 0.2` to use this library in your QML files Sample Usage ------------ - You can do simple filtering and sorting with SortFilterProxyModel: ```qml import QtQuick 2.2 import QtQuick.Controls 1.2 import SortFilterProxyModel 0.2 ApplicationWindow { visible: true width: 640 height: 480 ListModel { id: personModel ListElement { firstName: "Erwan" lastName: "Castex" favorite: true } // ... } TextField { id: textField anchors { top: parent.top; left: parent.left; right: parent.right } height: implicitHeight } SortFilterProxyModel { id: personProxyModel sourceModel: personModel filters: RegExpFilter { roleName: "lastName" pattern: textField.text caseSensitivity: Qt.CaseInsensitive } sorters: StringSorter { roleName: "firstName" } } ListView { anchors { top: textField.bottom; bottom: parent.bottom; left: parent.left; right: parent.right } model: personProxyModel delegate: Text { text: model.firstName + " " + model.lastName} } } ``` Here the `ListView` will only show elements that contains the content of the `TextField` in their `lastName` role. - But you can also achieve more complex filtering or sorting with multiple `filters` and `sorters`: ```qml SortFilterProxyModel { id: personProxyModel sourceModel: personModel filters: [ ValueFilter { enabled: onlyShowFavoritesCheckbox.checked roleName: "favorite" value: true }, AnyOf { RegExpFilter { roleName: "lastName" pattern: textField.text caseSensitivity: Qt.CaseInsensitive } RegExpFilter { roleName: "firstName" pattern: textField.text caseSensitivity: Qt.CaseInsensitive } } ] sorters: [ RoleSorter { roleName: "favorite"; sortOrder: Qt.DescendingOrder }, StringSorter { roleName: "firstName" }, StringSorter { roleName: "lastName" } ] } CheckBox { id:onlyShowFavoritesCheckbox } ``` This will show in the corresponding `ListView` only the elements where the `firstName` or the `lastName` match the text entered in the `textField`, and if the `onlyShowFavoritesCheckbox` is checked it will aditionnally filter the elements where `favorite` is `true`. The favorited elements will be shown first and all the elements are sorted by `firstName` and then `lastName`. Showcase Application -------------------- You can find an application showcasing this library here: https://github.com/oKcerG/SFPMShowcase License ------- This library is licensed under the MIT License. Documentation ------------- This component is a subclass of [`QSortFilterProxyModel`](http://doc.qt.io/qt-5/qsortfilterproxymodel.html), to use it, you need to set the `sourceModel` property to a [`QAbstractItemModel*`](http://doc.qt.io/qt-5/qabstractitemmodel.html) with correct role names. This means you can use it with custom c++ models or `ListModel`, but not with JavaScript models like arrays, integers or object instances. The complete documentation reference is available here: https://okcerg.github.io/SortFilterProxyModel/ Contributing ------------ Don't hesitate to open an issue about a suggestion, a bug, a lack of clarity in the documentation, etc. Pull requests are also welcome, if it's a important change you should open an issue first though. spectral/include/SortFilterProxyModel/.gitignore0000644000175000000620000000144413566674121022121 0ustar dilingerstaff# This file is used to ignore files which are generated # ---------------------------------------------------------------------------- *~ *.autosave *.a *.core *.moc *.o *.obj *.orig *.rej *.so *.so.* *_pch.h.cpp *_resource.rc *.qm .#* *.*# core !core/ tags .DS_Store .directory *.debug Makefile* *.prl *.app moc_*.cpp ui_*.h qrc_*.cpp *.qmlc Thumbs.db *.res *.rc /.qmake.cache /.qmake.stash # qtcreator generated files *.pro.user* # qtcreator shadow builds build-SortFilterProxyModel-* # xemacs temporary files *.flc # Vim temporary files .*.swp # Visual Studio generated files *.ib_pdb_index *.idb *.ilk *.pdb *.sln *.suo *.vcproj *vcproj.*.*.user *.ncb *.sdf *.opensdf *.vcxproj *vcxproj.* # MinGW generated files *.Debug *.Release # Python byte code *.pyc # Binaries # -------- *.dll *.exe spectral/include/SortFilterProxyModel/tests/0002755000175000000620000000000013566674121021272 5ustar dilingerstaffspectral/include/SortFilterProxyModel/tests/tst_helpers.qml0000644000175000000620000000663413566674121024350 0ustar dilingerstaffimport QtQuick 2.0 import QtQml 2.2 import QtTest 1.1 import SortFilterProxyModel 0.2 import SortFilterProxyModel.Test 0.2 Item { ListModel { id: dataModel ListElement { firstName: "Tupac" lastName: "Shakur" } ListElement { firstName: "Charles" lastName: "Aznavour" } ListElement { firstName: "Frank" lastName: "Sinatra" } ListElement { firstName: "Laurent" lastName: "Garnier" } ListElement { firstName: "Phillipe" lastName: "Risoli" } } SortFilterProxyModel { id: testModel sourceModel: dataModel } SortFilterProxyModel { id: testModel2 sourceModel: dataModel filters: ValueFilter { inverted: true roleName: "lastName" value: "Sinatra" } sorters: [ RoleSorter { roleName: "lastName"}, RoleSorter { roleName: "firstName"} ] } TestCase { name: "Helper functions" function test_getWithRoleName() { compare(testModel.get(0, "lastName"), "Shakur"); } function test_getWithoutRoleName() { compare(testModel.get(1), { firstName: "Charles", lastName: "Aznavour"}); } function test_roleForName() { compare(testModel.data(testModel.index(0, 0), testModel.roleForName("firstName")), "Tupac"); compare(testModel.data(testModel.index(1, 0), testModel.roleForName("lastName")), "Aznavour"); } function test_mapToSource() { compare(testModel2.mapToSource(3), 0); compare(testModel2.mapToSource(4), -1); } function test_mapToSourceLoop() { for (var i = 0; i < testModel2.count; ++i) { var sourceRow = testModel2.mapToSource(i); compare(testModel2.get(i).lastName, dataModel.get(sourceRow).lastName); } } function test_mapToSourceLoop_index() { for (var i = 0; i < testModel2.count; ++i) { var proxyIndex = testModel2.index(i, 0); var sourceIndex = testModel2.mapToSource(proxyIndex); var roleNumber = testModel2.roleForName("lastName"); compare(testModel2.data(proxyIndex, roleNumber), dataModel.data(sourceIndex, roleNumber)); } } function test_mapFromSource() { compare(testModel2.mapFromSource(1), 0); compare(testModel2.mapFromSource(2), -1); } function test_mapFromSourceLoop() { for (var i = 0; i < dataModel.count; ++i) { var proxyRow = testModel2.mapFromSource(i); if (proxyRow !== -1) { compare(dataModel.get(i).lastName, testModel2.get(proxyRow).lastName); } } } function test_mapFromSourceLoop_index() { for (var i = 0; i < dataModel.count; ++i) { var sourceIndex = dataModel.index(i, 0); var proxyIndex = testModel2.mapFromSource(sourceIndex); var roleNumber = testModel2.roleForName("lastName"); if (proxyIndex.valid) compare(testModel2.data(proxyIndex, roleNumber), dataModel.data(sourceIndex, roleNumber)); } } } } spectral/include/SortFilterProxyModel/tests/tst_regexprole.qml0000644000175000000620000000343013566674121025051 0ustar dilingerstaffimport QtQuick 2.0 import QtQml 2.2 import QtTest 1.1 import SortFilterProxyModel 0.2 import QtQml 2.2 Item { ListModel { id: listModel ListElement { dummyRole: false; compoundRole: "0 - zero"; unusedRole: "" } ListElement { dummyRole: false; compoundRole: "1 - one"; unusedRole: "" } ListElement { dummyRole: false; compoundRole: "2 - two"; unusedRole: "" } ListElement { dummyRole: false; compoundRole: "3 - three"; unusedRole: "" } ListElement { dummyRole: false; compoundRole: "four"; unusedRole: "" } } SortFilterProxyModel { id: testModel sourceModel: listModel proxyRoles: [ RegExpRole { id: regExpRole roleName: "compoundRole" pattern: "(?\\d+) - (?.+)" }, RegExpRole { id: caseSensitiveRole roleName: "compoundRole" pattern: "\\d+ - (?[A-Z]+)" caseSensitivity: Qt.CaseSensitive }, RegExpRole { id: caseInsensitiveRole roleName: "compoundRole" pattern: "\\d+ - (?[A-Z]+)" caseSensitivity: Qt.CaseInsensitive } ] } TestCase { name: "RegExpRole" function test_regExpRole() { compare(testModel.get(0, "id"), "0"); compare(testModel.get(1, "id"), "1"); compare(testModel.get(0, "name"), "zero"); compare(testModel.get(4, "id"), undefined); compare(testModel.get(0, "nameCS"), undefined); compare(testModel.get(0, "nameCIS"), "zero"); } } } spectral/include/SortFilterProxyModel/tests/indexsorter.cpp0000644000175000000620000000140413566674121024341 0ustar dilingerstaff#include "indexsorter.h" #include int IndexSorter::compare(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const qqsfpm::QQmlSortFilterProxyModel& proxyModel) const { Q_UNUSED(proxyModel) return sourceLeft.row() - sourceRight.row(); } int ReverseIndexSorter::compare(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const qqsfpm::QQmlSortFilterProxyModel& proxyModel) const { Q_UNUSED(proxyModel) return sourceRight.row() - sourceLeft.row(); } void registerIndexSorterTypes() { qmlRegisterType("SortFilterProxyModel.Test", 0, 2, "IndexSorter"); qmlRegisterType("SortFilterProxyModel.Test", 0, 2, "ReverseIndexSorter"); } Q_COREAPP_STARTUP_FUNCTION(registerIndexSorterTypes) spectral/include/SortFilterProxyModel/tests/tst_filterrole.qml0000644000175000000620000000241013566674121025041 0ustar dilingerstaffimport QtQuick 2.0 import QtQml 2.2 import QtTest 1.1 import SortFilterProxyModel 0.2 import QtQml 2.2 Item { ListModel { id: listModel ListElement { name: "1"; age: 18 } ListElement { name: "2"; age: 22 } ListElement { name: "3"; age: 45 } ListElement { name: "4"; age: 10 } } SortFilterProxyModel { id: testModel sourceModel: listModel proxyRoles: FilterRole { name: "isOldEnough" RangeFilter { id: ageFilter roleName: "age" minimumInclusive: true minimumValue: 18 } } } TestCase { name: "FilterRole" function test_filterRole() { compare(testModel.get(0, "isOldEnough"), true); compare(testModel.get(1, "isOldEnough"), true); compare(testModel.get(2, "isOldEnough"), true); compare(testModel.get(3, "isOldEnough"), false); ageFilter.minimumValue = 21; compare(testModel.get(0, "isOldEnough"), false); compare(testModel.get(1, "isOldEnough"), true); compare(testModel.get(2, "isOldEnough"), true); compare(testModel.get(3, "isOldEnough"), false); } } } spectral/include/SortFilterProxyModel/tests/tst_joinrole.qml0000644000175000000620000000217213566674121024520 0ustar dilingerstaffimport QtQuick 2.0 import QtQml 2.2 import QtTest 1.1 import SortFilterProxyModel 0.2 import QtQml 2.2 Item { ListModel { id: listModel ListElement { firstName: "Justin"; lastName: "Timberlake" } } SortFilterProxyModel { id: testModel sourceModel: listModel proxyRoles: JoinRole { id: joinRole name: "fullName" roleNames: ["firstName", "lastName"] } } Instantiator { id: instantiator model: testModel QtObject { property string fullName: model.fullName } } TestCase { name: "JoinRole" function test_joinRole() { compare(instantiator.object.fullName, "Justin Timberlake"); listModel.setProperty(0, "lastName", "Bieber"); compare(instantiator.object.fullName, "Justin Bieber"); joinRole.roleNames = ["lastName", "firstName"]; compare(instantiator.object.fullName, "Bieber Justin"); joinRole.separator = " - "; compare(instantiator.object.fullName, "Bieber - Justin"); } } } spectral/include/SortFilterProxyModel/tests/tst_rolesorter.qml0000644000175000000620000000453113566674121025100 0ustar dilingerstaffimport QtQuick 2.0 import SortFilterProxyModel 0.2 import QtQml.Models 2.2 import QtTest 1.1 Item { property list sorters: [ RoleSorter { property string tag: "intRole" property var expectedValues: [1, 2, 3, 4, 5] roleName: "intRole" }, RoleSorter { property string tag: "intRoleDescending" property var expectedValues: [5, 4, 3, 2, 1] roleName: "intRole" sortOrder: Qt.DescendingOrder }, RoleSorter { property string tag: "stringRole" property var expectedValues: ["a", "b", "c", "d", "e"] roleName: "stringRole" }, RoleSorter { property string tag: "stringRoleDescending" property var expectedValues: ["e", "d", "c", "b", "a"] roleName: "stringRole" sortOrder: Qt.DescendingOrder }, RoleSorter { property string tag: "mixedCaseStringRole" property var expectedValues: ["A", "b", "C", "D", "e"] roleName: "mixedCaseStringRole" } ] ListModel { id: dataModel ListElement { intRole: 5; stringRole: "c"; mixedCaseStringRole: "C" } ListElement { intRole: 3; stringRole: "e"; mixedCaseStringRole: "e" } ListElement { intRole: 1; stringRole: "d"; mixedCaseStringRole: "D" } ListElement { intRole: 2; stringRole: "a"; mixedCaseStringRole: "A" } ListElement { intRole: 4; stringRole: "b"; mixedCaseStringRole: "b" } } SortFilterProxyModel { id: testModel sourceModel: dataModel } TestCase { name: "RoleSorterTests" function test_roleSorters_data() { return sorters; } function test_roleSorters(sorter) { testModel.sorters = sorter; verify(testModel.count === sorter.expectedValues.length, "Expected count " + sorter.expectedValues.length + ", actual count: " + testModel.count); for (var i = 0; i < testModel.count; i++) { var modelValue = testModel.get(i, sorter.roleName); verify(modelValue === sorter.expectedValues[i], "Expected testModel value " + sorter.expectedValues[i] + ", actual: " + modelValue); } } } } spectral/include/SortFilterProxyModel/tests/tst_sourceroles.qml0000644000175000000620000000221713566674121025244 0ustar dilingerstaffimport QtQuick 2.0 import QtTest 1.1 import QtQml 2.2 import SortFilterProxyModel 0.2 Item { ListModel { id: nonEmptyFirstModel ListElement { test: "test" } } SortFilterProxyModel { id: nonEmptyFirstProxyModel sourceModel: nonEmptyFirstModel } Instantiator { id: nonEmptyFirstInstantiator model: nonEmptyFirstProxyModel QtObject { property var test: model.test } } ListModel { id: emptyFirstModel } SortFilterProxyModel { id: emptyFirstProxyModel sourceModel: emptyFirstModel } Instantiator { id: emptyFirstInstantiator model: emptyFirstProxyModel QtObject { property var test: model.test } } TestCase { name: "RoleTests" function test_nonEmptyFirst() { compare(nonEmptyFirstInstantiator.object.test, "test"); } function test_emptyFirst() { emptyFirstModel.append({test: "test"}); compare(emptyFirstProxyModel.get(0), {test: "test"}); compare(emptyFirstInstantiator.object.test, "test"); } } } spectral/include/SortFilterProxyModel/tests/SortFilterProxyModel.pro0000644000175000000620000000134113566674121026131 0ustar dilingerstaffTEMPLATE = app TARGET = tst_sortfilterproxymodel QT += qml quick CONFIG += c++11 warn_on qmltestcase qml_debug no_keywords include(../SortFilterProxyModel.pri) HEADERS += \ indexsorter.h \ testroles.h SOURCES += \ tst_sortfilterproxymodel.cpp \ indexsorter.cpp \ testroles.cpp OTHER_FILES += \ tst_rangefilter.qml \ tst_indexfilter.qml \ tst_sourceroles.qml \ tst_sorters.qml \ tst_helpers.qml \ tst_builtins.qml \ tst_rolesorter.qml \ tst_stringsorter.qml \ tst_proxyroles.qml \ tst_joinrole.qml \ tst_switchrole.qml \ tst_expressionrole.qml DISTFILES += \ tst_filtercontainers.qml \ tst_regexprole.qml \ tst_filtersorter.qml \ tst_filterrole.qml spectral/include/SortFilterProxyModel/tests/tst_indexfilter.qml0000644000175000000620000000577713566674121025232 0ustar dilingerstaffimport QtQuick 2.0 import SortFilterProxyModel 0.2 import QtQml.Models 2.2 import QtTest 1.1 Item { property list filters: [ IndexFilter { property string tag: "basicUsage" property var expectedValues: [3, 1, 2] minimumIndex: 1; maximumIndex: 3 }, IndexFilter { property string tag: "outOfBounds" property var expectedValues: [] minimumIndex: 3; maximumIndex: 1 }, IndexFilter { property string tag: "0to0Inverted" property var expectedValues: [3,1,2,4] minimumIndex: 0; maximumIndex: 0; inverted: true }, IndexFilter { property string tag: "0to0" // bug / issue #15 property var expectedValues: [5] minimumIndex: 0; maximumIndex: 0 }, IndexFilter { property string tag: "basicUsageInverted" property var expectedValues: [5,4] minimumIndex: 1; maximumIndex: 3; inverted: true }, IndexFilter { property string tag: "last" property var expectedValues: [4] minimumIndex: -1 }, IndexFilter { property string tag: "fromEnd" property var expectedValues: [2, 4] minimumIndex: -2 }, IndexFilter { property string tag: "fromEndRange" property var expectedValues: [1, 2] minimumIndex: -3 maximumIndex: -2 }, IndexFilter { property string tag: "mixedSignRange" property var expectedValues: [3, 1, 2] minimumIndex: 1 maximumIndex: -2 }, IndexFilter { property string tag: "toBigFilter" property var expectedValues: [] minimumIndex: 5 }, IndexFilter { property string tag: "noFilter" property var expectedValues: [5, 3, 1, 2, 4] }, IndexFilter { property string tag: "undefinedFilter" property var expectedValues: [5, 3, 1, 2, 4] minimumIndex: undefined maximumIndex: null } ] ListModel { id: dataModel ListElement { value: 5 } ListElement { value: 3 } ListElement { value: 1 } ListElement { value: 2 } ListElement { value: 4 } } SortFilterProxyModel { id: testModel // FIXME: Crashes/fails with error if I define ListModel directly within sourceModel sourceModel: dataModel } TestCase { name: "IndexFilterTests" function test_minMax_data() { return filters; } function test_minMax(filter) { testModel.filters = filter; var actualValues = []; for (var i = 0; i < testModel.count; i++) actualValues.push(testModel.data(testModel.index(i, 0))); compare(actualValues, filter.expectedValues); } } } spectral/include/SortFilterProxyModel/tests/indexsorter.h0000644000175000000620000000110313566674121024002 0ustar dilingerstaff#ifndef INDEXSORTER_H #define INDEXSORTER_H #include "sorters/sorter.h" class IndexSorter : public qqsfpm::Sorter { public: using qqsfpm::Sorter::Sorter; int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const qqsfpm::QQmlSortFilterProxyModel& proxyModel) const override; }; class ReverseIndexSorter : public qqsfpm::Sorter { public: using qqsfpm::Sorter::Sorter; int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const qqsfpm::QQmlSortFilterProxyModel& proxyModel) const override; }; #endif // INDEXSORTER_H spectral/include/SortFilterProxyModel/tests/tst_expressionrole.qml0000644000175000000620000000173013566674121025757 0ustar dilingerstaffimport QtQuick 2.0 import QtQml 2.2 import QtTest 1.1 import SortFilterProxyModel 0.2 import QtQml 2.2 Item { property int c: 0 ListModel { id: listModel ListElement { a: 1; b: 2 } } SortFilterProxyModel { id: testModel sourceModel: listModel proxyRoles: ExpressionRole { name: "expressionRole" expression: a + model.b + c } } Instantiator { id: instantiator model: testModel QtObject { property string expressionRole: model.expressionRole } } TestCase { name: "ExpressionRole" function test_expressionRole() { fuzzyCompare(instantiator.object.expressionRole, 3, 1e-7); listModel.setProperty(0, "b", 9); fuzzyCompare(instantiator.object.expressionRole, 10, 1e-7); c = 1327; fuzzyCompare(instantiator.object.expressionRole, 1337, 1e-7); } } } spectral/include/SortFilterProxyModel/tests/tst_sortfilterproxymodel.cpp0000644000175000000620000000011113566674121027177 0ustar dilingerstaff#include QUICK_TEST_MAIN(SortFilterProxyModel) spectral/include/SortFilterProxyModel/tests/tst_filtercontainers.qml0000644000175000000620000000526513566674121026260 0ustar dilingerstaffimport QtQuick 2.0 import SortFilterProxyModel 0.2 import QtQml.Models 2.2 import QtTest 1.1 Item { property list filters: [ AllOf { property string tag: "allOf" property var expectedValues: [{a: 0, b: false}] ValueFilter { roleName: "a" value: "0" } ValueFilter { roleName: "b" value: false } }, AllOf { property string tag: "allOfOneDisabled" property var expectedValues: [{a: 0, b: true}, {a: 0, b: false}] ValueFilter { roleName: "a" value: "0" } ValueFilter { enabled: false roleName: "b" value: false } }, AnyOf { property string tag: "anyOf" property var expectedValues: [{a: 0, b: true}, {a: 0, b: false}, {a: 1, b: false}] ValueFilter { roleName: "a" value: "0" } ValueFilter { roleName: "b" value: false } } ] AllOf { id: outerFilter ValueFilter { roleName: "a" value: "0" } ValueFilter { id: innerFilter roleName: "b" value: false } } ListModel { id: dataModel ListElement { a: 0; b: true } ListElement { a: 0; b: false } ListElement { a: 1; b: true } ListElement { a: 1; b: false } } SortFilterProxyModel { id: testModel sourceModel: dataModel } TestCase { name:"RangeFilterTests" function modelValues() { var modelValues = []; for (var i = 0; i < testModel.count; i++) modelValues.push(testModel.get(i)); return modelValues; } function test_filterContainers_data() { return filters; } function test_filterContainers(filter) { testModel.filters = filter; compare(JSON.stringify(modelValues()), JSON.stringify(filter.expectedValues)); } function test_changeInnerFilter() { testModel.filters = outerFilter; compare(JSON.stringify(modelValues()), JSON.stringify([{a: 0, b: false}])); innerFilter.value = true; compare(JSON.stringify(modelValues()), JSON.stringify([{a: 0, b: true}])); innerFilter.enabled = false; compare(JSON.stringify(modelValues()), JSON.stringify([{a: 0, b: true}, {a: 0, b: false}])); } } } spectral/include/SortFilterProxyModel/tests/tst_filtersorter.qml0000644000175000000620000000224513566674121025424 0ustar dilingerstaffimport QtQuick 2.0 import QtQml 2.2 import QtTest 1.1 import SortFilterProxyModel 0.2 import QtQml 2.2 Item { ListModel { id: listModel ListElement { name: "1"; favorite: true } ListElement { name: "2"; favorite: false } ListElement { name: "3"; favorite: false } ListElement { name: "4"; favorite: true } } SortFilterProxyModel { id: testModel sourceModel: listModel sorters: FilterSorter { ValueFilter { id: favoriteFilter roleName: "favorite" value: true } } } TestCase { name: "FilterSorter" function test_filterSorter() { compare(testModel.get(0, "name"), "1"); compare(testModel.get(1, "name"), "4"); compare(testModel.get(2, "name"), "2"); compare(testModel.get(3, "name"), "3"); favoriteFilter.value = false; compare(testModel.get(0, "name"), "2"); compare(testModel.get(1, "name"), "3"); compare(testModel.get(2, "name"), "1"); compare(testModel.get(3, "name"), "4"); } } } spectral/include/SortFilterProxyModel/tests/tst_rangefilter.qml0000644000175000000620000000676713566674121025217 0ustar dilingerstaffimport QtQuick 2.0 import SortFilterProxyModel 0.2 import QtQml.Models 2.2 import QtTest 1.1 Item { property list filters: [ RangeFilter { property string tag: "inclusive" property int expectedModelCount: 3 property var expectedValues: [3, 2, 4] property QtObject dataModel: dataModel0 roleName: "value"; minimumValue: 2; maximumValue: 4 }, RangeFilter { property string tag: "explicitInclusive" property int expectedModelCount: 3 property var expectedValues: [3, 2, 4] property QtObject dataModel: dataModel0 roleName: "value"; minimumValue: 2; maximumValue: 4; minimumInclusive: true; maximumInclusive: true }, RangeFilter { property string tag: "inclusiveMinExclusiveMax" property int expectedModelCount: 2 property var expectedValues: [2, 3] property QtObject dataModel: dataModel1 roleName: "value"; minimumValue: 2; maximumValue: 4; minimumInclusive: true; maximumInclusive: false }, RangeFilter { property string tag: "exclusiveMinInclusiveMax" property int expectedModelCount: 2 property var expectedValues: [3, 4] property QtObject dataModel: dataModel1 roleName: "value"; minimumValue: 2; maximumValue: 4; minimumInclusive: false; maximumInclusive: true }, RangeFilter { property string tag: "exclusive" property int expectedModelCount: 1 property var expectedValues: [3] property QtObject dataModel: dataModel1 roleName: "value"; minimumValue: 2; maximumValue: 4; minimumInclusive: false; maximumInclusive: false }, RangeFilter { property string tag: "outOfBoundsRange" property var expectedValues: [] property QtObject dataModel: dataModel1 roleName: "value"; minimumValue: 4; maximumValue: 2 }, RangeFilter { objectName: tag property string tag: "noMinimum" property var expectedValues: [3, 1, 2] property QtObject dataModel: dataModel0 roleName: "value"; maximumValue: 3 } ] ListModel { id: dataModel0 ListElement { value: 5 } ListElement { value: 3 } ListElement { value: 1 } ListElement { value: 2 } ListElement { value: 4 } } ListModel { id: dataModel1 ListElement { value: 5 } ListElement { value: 2 } ListElement { value: 3 } ListElement { value: 1 } ListElement { value: 4 } } SortFilterProxyModel { id: testModel } TestCase { name:"RangeFilterTests" function test_minMax_data() { return filters; } function test_minMax(filter) { testModel.sourceModel = filter.dataModel; testModel.filters = filter; verify(testModel.count === filter.expectedValues.length, "Expected count " + filter.expectedValues.length + ", actual count: " + testModel.count); for (var i = 0; i < testModel.count; i++) { var modelValue = testModel.get(i, filter.roleName); verify(modelValue === filter.expectedValues[i], "Expected testModel value " + filter.expectedValues[i] + ", actual: " + modelValue); } } } } spectral/include/SortFilterProxyModel/tests/tst_stringsorter.qml0000644000175000000620000000632313566674121025446 0ustar dilingerstaffimport QtQuick 2.0 import SortFilterProxyModel 0.2 import QtQml.Models 2.2 import QtTest 1.1 Item { property list sorters: [ StringSorter { property string tag: "normal" property var expectedValues: ["haha", "hähä", "hehe", "héhé", "hihi", "huhu"] roleName: "accentRole" }, StringSorter { property string tag: "numericMode" property var expectedValues: ["a1", "a20", "a30", "a99", "a100", "a1000"] roleName: "numericRole" numericMode: true }, StringSorter { property string tag: "nonNumericMode" property var expectedValues: ["a1", "a100", "a1000", "a20", "a30", "a99"] roleName: "numericRole" numericMode: false }, StringSorter { property string tag: "caseSensitive" property var expectedValues: ["a", "A", "b", "c", "z", "Z"] roleName: "caseRole" caseSensitivity: Qt.CaseSensitive }, StringSorter { property string tag: "nonCaseSensitive" property var expectedValues: ["A", "a", "b", "c", "Z", "z"] roleName: "caseRole" caseSensitivity: Qt.CaseInsensitive }, StringSorter { property string tag: "ignorePunctuation" property var expectedValues: ["a-a", "aa", "b-b", "b-c", "b.c", "bc"] roleName: "punctuationRole" ignorePunctation: true }, StringSorter { property string tag: "doNotIgnorePunctuation" property var expectedValues: ["aa", "a-a", "b.c", "b-b", "bc", "b-c"] roleName: "punctuationRole" ignorePunctation: false } ] ListModel { id: dataModel ListElement { accentRole: "héhé"; numericRole: "a20"; caseRole: "b"; punctuationRole: "a-a"} ListElement { accentRole: "hehe"; numericRole: "a1"; caseRole: "A"; punctuationRole: "aa"} ListElement { accentRole: "haha"; numericRole: "a100"; caseRole: "a"; punctuationRole: "b-c"} ListElement { accentRole: "huhu"; numericRole: "a99"; caseRole: "c"; punctuationRole: "b.c"} ListElement { accentRole: "hihi"; numericRole: "a30"; caseRole: "Z"; punctuationRole: "bc"} ListElement { accentRole: "hähä"; numericRole: "a1000"; caseRole: "z"; punctuationRole: "b-b"} } SortFilterProxyModel { id: testModel sourceModel: dataModel } TestCase { name: "StringSorterTests" function test_stringSorters_data() { return sorters; } function test_stringSorters(sorter) { testModel.sorters = sorter; verify(testModel.count === sorter.expectedValues.length, "Expected count " + sorter.expectedValues.length + ", actual count: " + testModel.count); for (var i = 0; i < testModel.count; i++) { var modelValue = testModel.get(i, sorter.roleName); verify(modelValue === sorter.expectedValues[i], "Expected testModel value " + sorter.expectedValues[i] + ", actual: " + modelValue); } } } } spectral/include/SortFilterProxyModel/tests/testroles.cpp0000644000175000000620000000222713566674121024023 0ustar dilingerstaff#include "testroles.h" #include QVariant StaticRole::value() const { return m_value; } void StaticRole::setValue(const QVariant& value) { if (m_value == value) return; m_value = value; Q_EMIT valueChanged(); invalidate(); } QVariant StaticRole::data(const QModelIndex& sourceIndex, const qqsfpm::QQmlSortFilterProxyModel& proxyModel) { Q_UNUSED(sourceIndex) Q_UNUSED(proxyModel) return m_value; } QVariant SourceIndexRole::data(const QModelIndex& sourceIndex, const qqsfpm::QQmlSortFilterProxyModel& proxyModel) { Q_UNUSED(proxyModel) return sourceIndex.row(); } QStringList MultiRole::names() { return {"role1", "role2"}; } QVariant MultiRole::data(const QModelIndex&, const qqsfpm::QQmlSortFilterProxyModel&, const QString& name) { return "data for " + name; } void registerTestRolesTypes() { qmlRegisterType("SortFilterProxyModel.Test", 0, 2, "StaticRole"); qmlRegisterType("SortFilterProxyModel.Test", 0, 2, "SourceIndexRole"); qmlRegisterType("SortFilterProxyModel.Test", 0, 2, "MultiRole"); } Q_COREAPP_STARTUP_FUNCTION(registerTestRolesTypes) spectral/include/SortFilterProxyModel/tests/tst_sorters.qml0000644000175000000620000001201513566674121024375 0ustar dilingerstaffimport QtQuick 2.0 import QtQml 2.2 import QtTest 1.1 import SortFilterProxyModel 0.2 import SortFilterProxyModel.Test 0.2 Item { ListModel { id: listModel ListElement { test: "first"; test2: "c" } ListElement { test: "second"; test2: "a" } ListElement { test: "third"; test2: "b" } ListElement { test: "fourth"; test2: "b" } } ListModel { id: noRolesFirstListModel } property list sorters: [ QtObject { property string tag: "no sorter" property bool notASorter: true property var expectedValues: ["first", "second", "third", "fourth"] }, IndexSorter { property string tag: "Dummy IndexSorter" property var expectedValues: ["first", "second", "third", "fourth"] }, ReverseIndexSorter { property string tag: "Dummy ReverseIndexSorter" property var expectedValues: ["fourth", "third", "second", "first"] }, IndexSorter { property string tag: "Disabled dummy IndexSorter" enabled: false property var expectedValues: ["first", "second", "third", "fourth"] }, ReverseIndexSorter { property string tag: "Disabled dummy ReverseIndexSorter" enabled: false property var expectedValues: ["first", "second", "third", "fourth"] }, IndexSorter { property string tag: "Descending dummy IndexSorter" ascendingOrder: false property var expectedValues: ["fourth", "third", "second", "first"] }, ReverseIndexSorter { property string tag: "Descending dummy ReverseIndexSorter" ascendingOrder: false property var expectedValues: ["first", "second", "third", "fourth"] }, IndexSorter { property string tag: "Disabled descending dummy IndexSorter" enabled: false ascendingOrder: false property var expectedValues: ["first", "second", "third", "fourth"] }, ReverseIndexSorter { property string tag: "Disabled descending dummy ReverseIndexSorter" enabled: false ascendingOrder: false property var expectedValues: ["first", "second", "third", "fourth"] } ] ReverseIndexSorter { id: reverseIndexSorter } property list tieSorters: [ RoleSorter { roleName: "test2" }, RoleSorter { roleName: "test" } ] SortFilterProxyModel { id: testModel sourceModel: listModel } SortFilterProxyModel { id: noRolesFirstProxyModel sourceModel: noRolesFirstListModel sorters: RoleSorter { roleName: "test" } } TestCase { name: "SortersTests" function test_indexOrder_data() { return sorters; } function test_indexOrder(sorter) { testModel.sorters = sorter; verifyModelValues(testModel, sorter.expectedValues); } function test_enablingSorter() { reverseIndexSorter.enabled = false; testModel.sorters = reverseIndexSorter; var expectedValuesBeforeEnabling = ["first", "second", "third", "fourth"]; var expectedValuesAfterEnabling = ["fourth", "third", "second", "first"]; verifyModelValues(testModel, expectedValuesBeforeEnabling); reverseIndexSorter.enabled = true; verifyModelValues(testModel, expectedValuesAfterEnabling); } function test_disablingSorter() { reverseIndexSorter.enabled = true; testModel.sorters = reverseIndexSorter; var expectedValuesBeforeDisabling = ["fourth", "third", "second", "first"]; var expectedValuesAfterDisabling = ["first", "second", "third", "fourth"]; verifyModelValues(testModel, expectedValuesBeforeDisabling); reverseIndexSorter.enabled = false; verifyModelValues(testModel, expectedValuesAfterDisabling); } function test_tieSorters() { testModel.sorters = tieSorters; var expectedValues = ["second", "fourth", "third", "first"]; verifyModelValues(testModel, expectedValues); } function test_noRolesFirstModel() { noRolesFirstListModel.append([{test: "b"}, {test: "a"}]); var expectedValues = ["a", "b"]; verifyModelValues(noRolesFirstProxyModel, expectedValues); } function verifyModelValues(model, expectedValues) { verify(model.count === expectedValues.length, "Expected count " + expectedValues.length + ", actual count: " + model.count); for (var i = 0; i < model.count; i++) { var modelValue = model.get(i, "test"); verify(modelValue === expectedValues[i], "Expected testModel value " + expectedValues[i] + ", actual: " + modelValue); } } } } spectral/include/SortFilterProxyModel/tests/testroles.h0000644000175000000620000000206513566674121023470 0ustar dilingerstaff#ifndef TESTROLES_H #define TESTROLES_H #include "proxyroles/singlerole.h" #include class StaticRole : public qqsfpm::SingleRole { Q_OBJECT Q_PROPERTY(QVariant value READ value WRITE setValue NOTIFY valueChanged) public: using qqsfpm::SingleRole::SingleRole; QVariant value() const; void setValue(const QVariant& value); Q_SIGNALS: void valueChanged(); protected: private: QVariant data(const QModelIndex& sourceIndex, const qqsfpm::QQmlSortFilterProxyModel& proxyModel) override; QVariant m_value; }; class SourceIndexRole : public qqsfpm::SingleRole { public: using qqsfpm::SingleRole::SingleRole; private: QVariant data(const QModelIndex& sourceIndex, const qqsfpm::QQmlSortFilterProxyModel& proxyModel) override; }; class MultiRole : public qqsfpm::ProxyRole { public: using qqsfpm::ProxyRole::ProxyRole; QStringList names() override; private: QVariant data(const QModelIndex &sourceIndex, const qqsfpm::QQmlSortFilterProxyModel &proxyModel, const QString &name) override; }; #endif // TESTROLES_H spectral/include/SortFilterProxyModel/tests/tst_builtins.qml0000644000175000000620000000333013566674121024525 0ustar dilingerstaffimport QtQuick 2.0 import QtQml 2.2 import QtTest 1.1 import SortFilterProxyModel 0.2 Item { ListModel { id: testModel ListElement{role1: 1; role2: 1} ListElement{role1: 2; role2: 1} ListElement{role1: 3; role2: 2} ListElement{role1: 4; role2: 2} } ListModel { id: noRolesFirstTestModel function initModel() { noRolesFirstTestModel.append({role1: 1, role2: 1 }) noRolesFirstTestModel.append({role1: 2, role2: 1 }) noRolesFirstTestModel.append({role1: 3, role2: 2 }) noRolesFirstTestModel.append({role1: 4, role2: 2 }) } } SortFilterProxyModel { id: testProxyModel property string tag: "testProxyModel" sourceModel: testModel filterRoleName: "role2" filterValue: 2 property var expectedData: ([{role1: 3, role2: 2}, {role1: 4, role2: 2}]) } SortFilterProxyModel { id: noRolesFirstTestProxyModel property string tag: "noRolesFirstTestProxyModel" sourceModel: noRolesFirstTestModel filterRoleName: "role2" filterValue: 2 property var expectedData: ([{role1: 3, role2: 2}, {role1: 4, role2: 2}]) } TestCase { name: "BuiltinsFilterTests" function test_filterValue_data() { return [testProxyModel, noRolesFirstTestProxyModel]; } function test_filterValue(proxyModel) { if (proxyModel.sourceModel.initModel) proxyModel.sourceModel.initModel() var data = []; for (var i = 0; i < proxyModel.count; i++) data.push(proxyModel.get(i)); compare(data, proxyModel.expectedData); } } } spectral/include/SortFilterProxyModel/tests/tst_switchrole.qml0000644000175000000620000000573213566674121025067 0ustar dilingerstaffimport QtQuick 2.0 import QtQml 2.2 import QtTest 1.1 import SortFilterProxyModel 0.2 import QtQml 2.2 Item { ListModel { id: listModel ListElement { name: "1"; favorite: true } ListElement { name: "2"; favorite: false } ListElement { name: "3"; favorite: false } ListElement { name: "4"; favorite: true } } SortFilterProxyModel { id: testModel sourceModel: listModel proxyRoles: SwitchRole { id: switchRole name: "switchRole" ValueFilter { id: valueFilter roleName: "favorite" value: true SwitchRole.value: "*" } ValueFilter { id: secondValueFilter roleName: "favorite" value: true SwitchRole.value: "%" } ValueFilter { id: thirdValueFilter roleName: "name" value: 3 SwitchRole.value: "three" } defaultRoleName: "name" defaultValue: "foo" } } Instantiator { id: instantiator model: testModel QtObject { property var switchRole: model.switchRole } } TestCase { name: "SwitchRole" function test_role() { compare(testModel.get(0, "switchRole"), "*"); compare(testModel.get(1, "switchRole"), "2"); compare(testModel.get(2, "switchRole"), "three"); compare(testModel.get(3, "switchRole"), "*"); } function test_valueChange() { compare(instantiator.object.switchRole, "*"); valueFilter.SwitchRole.value = "test"; compare(instantiator.object.switchRole, "test"); valueFilter.SwitchRole.value = "*"; } function test_filterChange() { compare(instantiator.object.switchRole, "*"); valueFilter.enabled = false; compare(instantiator.object.switchRole, "%"); valueFilter.enabled = true; } function test_defaultSourceChange() { compare(instantiator.object.switchRole, "*"); listModel.setProperty(0, "favorite", false); compare(instantiator.object.switchRole, "1"); compare(instantiator.objectAt(1).switchRole, "2"); listModel.setProperty(1, "name", "test"); compare(instantiator.objectAt(1).switchRole, "test"); listModel.setProperty(1, "name", "2"); listModel.setProperty(0, "favorite", true); } function test_defaultValue() { switchRole.defaultRoleName = ""; compare(instantiator.objectAt(1).switchRole, "foo"); switchRole.defaultValue = "bar"; compare(instantiator.objectAt(1).switchRole, "bar"); switchRole.defaultRoleName = "name"; switchRole.defaultValue = "foo"; } } } spectral/include/SortFilterProxyModel/tests/tst_proxyroles.qml0000644000175000000620000000674413566674121025136 0ustar dilingerstaffimport QtQuick 2.0 import QtQml 2.2 import QtTest 1.1 import SortFilterProxyModel 0.2 import SortFilterProxyModel.Test 0.2 import QtQml 2.2 Item { ListModel { id: listModel ListElement { test: "first"; keep: true } ListElement { test: "second"; keep: true } ListElement { test: "third"; keep: true } } SortFilterProxyModel { id: testModel sourceModel: listModel filters: [ ValueFilter { roleName: "keep" value: true }, ValueFilter { inverted: true roleName: "staticRole" value: "filterMe" } ] proxyRoles: [ StaticRole { id: staticRole name: "staticRole" value: "foo" }, StaticRole { id: renameRole name: "renameMe" value: "test" }, SourceIndexRole { name: "sourceIndexRole" }, MultiRole {} ] } Instantiator { id: instantiator model: testModel QtObject { property string staticRole: model.staticRole property int sourceIndexRole: model.sourceIndexRole } } ListModel { id: singleRowModel ListElement { changingRole: "Change me" otherRole: "I don't change" } } SortFilterProxyModel { id: noProxyRolesProxyModel sourceModel: singleRowModel } Instantiator { id: outerInstantiator model: noProxyRolesProxyModel QtObject { property var counter: ({ count : 0 }) property string changingRole: model.changingRole property string otherRole: { ++counter.count; return model.otherRole; } } } TestCase { name: "ProxyRoles" function test_resetAfterNameChange() { var oldObject = instantiator.object; renameRole.name = "foobarRole"; var newObject = instantiator.object; verify(newObject !== oldObject, "Instantiator object should have been reinstantiated"); } function test_proxyRoleInvalidation() { compare(instantiator.object.staticRole, "foo"); staticRole.value = "bar"; compare(instantiator.object.staticRole, "bar"); } function test_proxyRoleGetDataFromSource() { compare(instantiator.object.sourceIndexRole, 0); compare(testModel.get(1, "sourceIndexRole"), 1); listModel.setProperty(1, "keep", false); compare(testModel.get(1, "sourceIndexRole"), 2); } function test_filterFromProxyRole() { staticRole.value = "filterMe"; compare(testModel.count, 0); staticRole.value = "foo"; compare(testModel.count, 3); } function test_multiRole() { compare(testModel.get(0, "role1"), "data for role1"); compare(testModel.get(0, "role2"), "data for role2"); } function test_ProxyRolesDataChanged() { outerInstantiator.object.counter.count = 0; singleRowModel.setProperty(0, "changingRole", "Changed") compare(outerInstantiator.object.changingRole, "Changed"); compare(outerInstantiator.object.counter.count, 0); } } } spectral/include/SortFilterProxyModel/docs/0002755000175000000620000000000013566674121021060 5ustar dilingerstaffspectral/include/SortFilterProxyModel/docs/qml-sortfilterproxymodel-members.html0000644000175000000620000000447713566674121030517 0ustar dilingerstaff List of All Members for SortFilterProxyModel | SortFilterProxyModel

List of All Members for SortFilterProxyModel

spectral/include/SortFilterProxyModel/docs/qml-filtersorter-members.html0000644000175000000620000000200613566674121026705 0ustar dilingerstaff List of All Members for FilterSorter | SortFilterProxyModel

List of All Members for FilterSorter

This is the complete list of members for FilterSorter, including inherited members.

The following members are inherited from Sorter.

spectral/include/SortFilterProxyModel/docs/qml-switchrole-members.html0000644000175000000620000000235713566674121026355 0ustar dilingerstaff List of All Members for SwitchRole | SortFilterProxyModel

List of All Members for SwitchRole

This is the complete list of members for SwitchRole, including inherited members.

The following members are inherited from SingleRole.

spectral/include/SortFilterProxyModel/docs/qml-anyof.html0000644000175000000620000000453413566674121023655 0ustar dilingerstaff AnyOf QML Type | SortFilterProxyModel

AnyOf QML Type

Filter container accepting rows accepted by at least one of its child filters More...

Import Statement: import .
Inherits:

Filter

Detailed Description

The AnyOf type is a Filter container that accepts rows if any of its contained (and enabled) filters accept them.

In the following example, only the rows where the firstName role or the lastName role match the text entered in the nameTextField will be accepted :

TextField {
  id: nameTextField
}

SortFilterProxyModel {
  sourceModel: contactModel
  filters: AnyOf {
      RegExpFilter {
          roleName: "lastName"
          pattern: nameTextField.text
          caseSensitivity: Qt.CaseInsensitive
      }
      RegExpFilter {
          roleName: "firstName"
          pattern: nameTextField.text
          caseSensitivity: Qt.CaseInsensitive
      }
  }
}
spectral/include/SortFilterProxyModel/docs/qml-regexprole.html0000644000175000000620000001030613566674121024707 0ustar dilingerstaff RegExpRole QML Type | SortFilterProxyModel

RegExpRole QML Type

A ProxyRole extracting data from a source role via a regular expression. More...

Import Statement: import .
Inherits:

ProxyRole

Properties

Detailed Description

A RegExpRole is a ProxyRole that provides a role for each named capture group of its regular expression pattern.

In the following example, the date role of the source model will be extracted in 3 roles in the proxy moodel: year, month and day.

SortFilterProxyModel {
    sourceModel: eventModel
    proxyRoles: RegExpRole {
        roleName: "date"
        pattern: "(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})"
    }
}

Property Documentation

caseSensitivity : Qt::CaseSensitivity

This property holds the caseSensitivity of the regular expression.


pattern : QString

This property holds the pattern of the regular expression of this RegExpRole. The RegExpRole will expose a role for each of the named capture group of the pattern.


roleName : QString

This property holds the role name that the RegExpRole is using to query the source model's data to extract new roles from.


spectral/include/SortFilterProxyModel/docs/qml-valuefilter.html0000644000175000000620000000642013566674121025057 0ustar dilingerstaff ValueFilter QML Type | SortFilterProxyModel

ValueFilter QML Type

Filters rows matching exactly a value More...

Import Statement: import .
Inherits:

RoleFilter

Properties

Detailed Description

A ValueFilter is a simple RoleFilter that accepts rows matching exactly the filter's value

In the following example, only rows with their favorite role set to true will be accepted when the checkbox is checked :

CheckBox {
   id: showOnlyFavoriteCheckBox
}

SortFilterProxyModel {
   sourceModel: contactModel
   filters: ValueFilter {
       roleName: "favorite"
       value: true
       enabled: showOnlyFavoriteCheckBox.checked
   }
}

Property Documentation

roleName : string

This property holds the role name that the filter is using to query the source model's data when filtering items.


value : variant

This property holds the value used to filter the contents of the source model.


spectral/include/SortFilterProxyModel/docs/qml-expressionrole.html0000644000175000000620000000736213566674121025624 0ustar dilingerstaff ExpressionRole QML Type | SortFilterProxyModel

ExpressionRole QML Type

A custom role computed from a javascrip expression More...

Import Statement: import .
Inherits:

SingleRole

Properties

Detailed Description

An ExpressionRole is a ProxyRole allowing to implement a custom role based on a javascript expression.

In the following example, the c role is computed by adding the a role and b role of the model :

SortFilterProxyModel {
   sourceModel: numberModel
   proxyRoles: ExpressionRole {
       name: "c"
       expression: model.a + model.b
  }
}

Property Documentation

expression : expression

An expression to implement a custom role. It has the same syntax has a Property Binding except it will be evaluated for each of the source model's rows. The data for this role will be the retuned valued of the expression. Data for each row is exposed like for a delegate of a QML View.

This expression is reevaluated for a row every time its model data changes. When an external property (not index or in model) the expression depends on changes, the expression is reevaluated for every row of the source model. To capture the properties the expression depends on, the expression is first executed with invalid data and each property access is detected by the QML engine. This means that if a property is not accessed because of a conditional, it won't be captured and the expression won't be reevaluted when this property changes.

A workaround to this problem is to access all the properties the expressions depends unconditionally at the beggining of the expression.


spectral/include/SortFilterProxyModel/docs/qml-singlerole.html0000644000175000000620000000540613566674121024703 0ustar dilingerstaff SingleRole QML Type | SortFilterProxyModel

SingleRole QML Type

Base type for the SortFilterProxyModel proxy roles defining a single role More...

Import Statement: import .
Inherits:

ProxyRole

Inherited By:

ExpressionRole, FilterRole, JoinRole, and SwitchRole

Properties

Detailed Description

SingleRole is a convenience base class for proxy roles who define a single role. It cannot be used directly in a QML file. It exists to provide a set of common properties and methods, available across all the other proxy role types that inherit from it. Attempting to use the SingleRole type directly will result in an error.

Property Documentation

name : string

This property holds the role name of the proxy role.


spectral/include/SortFilterProxyModel/docs/qml-regexpfilter.html0000644000175000000620000001412213566674121025233 0ustar dilingerstaff RegExpFilter QML Type | SortFilterProxyModel

RegExpFilter QML Type

Filters rows matching a regular expression More...

Import Statement: import .
Inherits:

RoleFilter

Properties

Detailed Description

A RegExpFilter is a RoleFilter that accepts rows matching a regular rexpression.

In the following example, only rows with their lastName role beggining with the content of textfield the will be accepted:

TextField {
   id: nameTextField
}

SortFilterProxyModel {
   sourceModel: contactModel
   filters: RegExpFilter {
       roleName: "lastName"
       pattern: "^" + nameTextField.displayText
   }
}

Property Documentation

caseSensitivity : Qt::CaseSensitivity

This property holds the caseSensitivity of the filter.


pattern : bool

The pattern used to filter the contents of the source model.

See also syntax.


roleName : string

This property holds the role name that the filter is using to query the source model's data when filtering items.


syntax : enum

The pattern used to filter the contents of the source model.

Only the source model's value having their RoleFilter::roleName data matching this pattern with the specified syntax will be kept.

ConstantDescription
RegExpFilter.RegExpA rich Perl-like pattern matching syntax. This is the default.
RegExpFilter.WildcardThis provides a simple pattern matching syntax similar to that used by shells (command interpreters) for "file globbing".
RegExpFilter.FixedStringThe pattern is a fixed string. This is equivalent to using the RegExp pattern on a string in which all metacharacters are escaped.
RegExpFilter.RegExp2Like RegExp, but with greedy quantifiers.
RegExpFilter.WildcardUnixThis is similar to Wildcard but with the behavior of a Unix shell. The wildcard characters can be escaped with the character "".
RegExpFilter.W3CXmlSchema11The pattern is a regular expression as defined by the W3C XML Schema 1.1 specification.

See also pattern.


spectral/include/SortFilterProxyModel/docs/qml-sortfilterproxymodel.html0000644000175000000620000002626113566674121027062 0ustar dilingerstaff SortFilterProxyModel QML Type | SortFilterProxyModel

SortFilterProxyModel QML Type

Filters and sorts data coming from a source QAbstractItemModel More...

Import Statement: import .

Properties

Methods

Detailed Description

The SortFilterProxyModel type provides support for filtering and sorting data coming from a source model.

Property Documentation

count : int

The number of rows in the proxy model (not filtered out the source model)


filters : list<Filter>

This property holds the list of filters for this proxy model. To be included in the model, a row of the source model has to be accepted by all the top level filters of this list.

See also Filter.


proxyRoles : list<ProxyRole>

This property holds the list of proxy roles for this proxy model. Each proxy role adds a new custom role to the model.

See also ProxyRole.


sortRoleName : string

The role name of the source model's data used for the sorting.

See also sortRole and roleForName.


sorters : list<Sorter>

This property holds the list of sorters for this proxy model. The rows of the source model are sorted by the sorters of this list, in their order of insertion.

See also Sorter.


sourceModel : QAbstractItemModel*

The source model of this proxy model


Method Documentation

variant get(int row, string roleName)

Return the data for the given roleNamte of the item at row in the proxy model. This allows the role data to be read (not modified) from JavaScript. This equivalent to calling data(index(row, 0), roleForName(roleName)).


object get(int row)

Return the item at row in the proxy model as a map of all its roles. This allows the item data to be read (not modified) from JavaScript.


int mapFromSource(int sourceRow)

Returns the row in the SortFilterProxyModel given the sourceRow from the source model. Returns -1 if there is no corresponding row.


QModelIndex mapFromSource(QModelIndex sourceIndex)

Returns the model index in the SortFilterProxyModel given the sourceIndex from the source model.


int mapToSource(int proxyRow)

Returns the source model row corresponding to the given proxyRow from the SortFilterProxyModel. Returns -1 if there is no corresponding row.


index mapToSource(index proxyIndex)

Returns the source model index corresponding to the given proxyIndex from the SortFilterProxyModel.


int roleForName(string roleName)

Returns the role number for the given roleName. If no role is found for this roleName, -1 is returned.


spectral/include/SortFilterProxyModel/docs/qml-allof.html0000644000175000000620000000340213566674121023627 0ustar dilingerstaff AllOf QML Type | SortFilterProxyModel

AllOf QML Type

Filter container accepting rows accepted by all its child filters More...

Import Statement: import .
Inherits:

Filter

Detailed Description

The AllOf type is a Filter container that accepts rows if all of its contained (and enabled) filters accept them, or if it has no filter.

Using it as a top level filter has the same effect as putting all its child filters as top level filters. It can however be usefull to use an AllOf filter when nested in an AnyOf filter.

spectral/include/SortFilterProxyModel/docs/qml-rangefilter.html0000644000175000000620000001375313566674121025046 0ustar dilingerstaff RangeFilter QML Type | SortFilterProxyModel

RangeFilter QML Type

Filters rows between boundary values More...

Import Statement: import .
Inherits:

RoleFilter

Properties

Detailed Description

A RangeFilter is a RoleFilter that accepts rows if their data is between the filter's minimum and maximum value.

In the following example, only rows with their price role set to a value between the tow boundary of the slider will be accepted :

RangeSlider {
   id: priceRangeSlider
}

SortFilterProxyModel {
   sourceModel: priceModel
   filters: RangeFilter {
       roleName: "price"
       minimumValue: priceRangeSlider.first.value
       maximumValue: priceRangeSlider.second.value
   }
}

Property Documentation

maximumInclusive : int

This property holds whether the minimumValue is inclusive.

By default, the minimumValue is inclusive.

See also minimumValue.


maximumValue : int

This property holds the maximumValue of the filter. Rows with a value higher than maximumValue will be rejected.

By default, no value is set.

See also maximumInclusive.


minimumInclusive : int

This property holds whether the minimumValue is inclusive.

By default, the minimumValue is inclusive.

See also minimumValue.


minimumValue : int

This property holds the minimumValue of the filter. Rows with a value lower than minimumValue will be rejected.

By default, no value is set.

See also minimumInclusive.


roleName : string

This property holds the role name that the filter is using to query the source model's data when filtering items.


spectral/include/SortFilterProxyModel/docs/qml-rolefilter.html0000644000175000000620000000525013566674121024704 0ustar dilingerstaff RoleFilter QML Type | SortFilterProxyModel

RoleFilter QML Type

Base type for filters based on a source model role More...

Import Statement: import .
Inherits:

Filter

Inherited By:

RangeFilter, RegExpFilter, and ValueFilter

Properties

Detailed Description

The RoleFilter type cannot be used directly in a QML file. It exists to provide a set of common properties and methods, available across all the other filter types that inherit from it. Attempting to use the RoleFilter type directly will result in an error.

Property Documentation

roleName : string

This property holds the role name that the filter is using to query the source model's data when filtering items.


spectral/include/SortFilterProxyModel/docs/qml-anyof-members.html0000644000175000000620000000155713566674121025307 0ustar dilingerstaff List of All Members for AnyOf | SortFilterProxyModel

List of All Members for AnyOf

This is the complete list of members for AnyOf, including inherited members.

The following members are inherited from Filter.

spectral/include/SortFilterProxyModel/docs/qml-allof-members.html0000644000175000000620000000155713566674121025270 0ustar dilingerstaff List of All Members for AllOf | SortFilterProxyModel

List of All Members for AllOf

This is the complete list of members for AllOf, including inherited members.

The following members are inherited from Filter.

spectral/include/SortFilterProxyModel/docs/qml-proxyrole-members.html0000644000175000000620000000114213566674121026224 0ustar dilingerstaff List of All Members for ProxyRole | SortFilterProxyModel

List of All Members for ProxyRole

This is the complete list of members for ProxyRole, including inherited members.

spectral/include/SortFilterProxyModel/docs/qml-stringsorter.html0000644000175000000620000001130113566674121025274 0ustar dilingerstaff StringSorter QML Type | SortFilterProxyModel

StringSorter QML Type

Sorts rows based on a source model string role More...

Import Statement: import .
Inherits:

RoleSorter

Properties

Detailed Description

StringSorter is a specialized RoleSorter that sorts rows based on a source model string role. StringSorter compares strings according to a localized collation algorithm.

In the following example, rows with be sorted by their lastName role :

SortFilterProxyModel {
   sourceModel: contactModel
   sorters: StringSorter { roleName: "lastName" }
}

Property Documentation

caseSensitivity : Qt.CaseSensitivity

This property holds the case sensitivity of the sorter.


ignorePunctation : bool

This property holds whether the sorter ignores punctation. if ignorePunctuation is true, punctuation characters and symbols are ignored when determining sort order.

Note: This property is not currently supported on Apple platforms or if Qt is configured to not use ICU on Linux.


locale : Locale

This property holds the locale of the sorter.


numericMode : bool

This property holds whether the numeric mode of the sorter is enabled. This will enable proper sorting of numeric digits, so that e.g. 100 sorts after 99. By default this mode is off.


spectral/include/SortFilterProxyModel/docs/qml-filtersorter.html0000644000175000000620000000542213566674121025262 0ustar dilingerstaff FilterSorter QML Type | SortFilterProxyModel

FilterSorter QML Type

Sorts rows based on if they match filters More...

Import Statement: import .
Inherits:

Sorter

Properties

Detailed Description

A FilterSorter is a Sorter that orders row matching its filters before the rows not matching the filters.

In the following example, rows with their favorite role set to true will be ordered at the beginning :

SortFilterProxyModel {
    sourceModel: contactModel
    sorters: FilterSorter {
        ValueFilter { roleName: "favorite"; value: true }
    }
}

Property Documentation

filters : string

This property holds the list of filters for this filter sorter. If a row match all this FilterSorter's filters, it will be ordered before rows not matching all the filters.

See also Filter.


spectral/include/SortFilterProxyModel/docs/style/0002755000175000000620000000000013566674121022220 5ustar dilingerstaffspectral/include/SortFilterProxyModel/docs/style/offline.css0000644000175000000620000003114513566674121024356 0ustar dilingerstaffbody { font: normal 400 14px/1.2 Arial; margin-top: 85px; font-family: Arial, Helvetica; text-align: left; margin-left: 5px; margin-right: 5px; background-color: #fff; } p { line-height: 20px } img { margin-left: 0px; max-width: 800px; height: auto; } .content .border img { box-shadow:3px 3px 8px 3px rgba(200,200,200,0.5) } .content .border .player { box-shadow:3px 3px 8px 3px rgba(200,200,200,0.5) } .content .indexboxcont li { font: normal bold 13px/1 Verdana } .content .normallist li { font: normal 13px/1 Verdana } .descr { margin-top: 35px; margin-bottom: 45px; margin-left: 5px; text-align: left; vertical-align: top; } .name { max-width: 75%; font-weight: 100; } tt { text-align: left } /* ----------- links ----------- */ a:link { color: #007330; text-decoration: none; text-align: left; } a.qa-mark:target:before { content: "***"; color: #ff0000; } a:hover { color: #44a51c; text-align: left; } a:visited { color: #007330; text-align: left; } a:visited:hover { color: #44a51c; text-align: left; } /* ----------- offline viewing: HTML links display an icon ----------- */ a[href*="http://"], a[href*="ftp://"], a[href*="https://"] { text-decoration: none; background-image: url(../images/ico_out.png); background-repeat: no-repeat; background-position: left; padding-left: 20px; text-align: left; } .flags { text-decoration: none; text-height: 24px; } .flags:target { background-color: #FFFFD6; } /* ------------------------------- NOTE styles ------------------------------- */ .notetitle, .tiptitle, .fastpathtitle { font-weight: bold } .attentiontitle, .cautiontitle, .dangertitle, .importanttitle, .remembertitle, .restrictiontitle { font-weight: bold } .note, .tip, .fastpath { background: #F2F2F2 url(../images/ico_note.png); background-repeat: no-repeat; background-position: top left; padding: 5px; padding-left: 40px; padding-bottom: 10px; border: #999 1px dotted; color: #666666; margin: 5px; } .attention, .caution, .danger, .important, .remember, .restriction { background: #F2F2F2 url(../images/ico_note_attention.png); background-repeat: no-repeat; background-position: top left; padding: 5px; padding-left: 40px; padding-bottom: 10px; border: #999 1px dotted; color: #666666; margin: 5px; } /* ------------------------------- Top navigation ------------------------------- */ .qtref { display: block; position: relative; height: 15px; z-index: 1; font-size: 11px; padding-right: 10px; float: right; } .naviNextPrevious { clear: both; display: block; position: relative; text-align: right; top: -47px; float: right; height: 20px; z-index: 1; padding-right: 10px; padding-top: 2px; vertical-align: top; margin: 0px; } .naviNextPrevious > a:first-child { background-image: url(../images/btn_prev.png); background-repeat: no-repeat; background-position: left; padding-left: 20px; height: 20px; padding-left: 20px; } .naviNextPrevious > a:last-child { background-image: url(../images/btn_next.png); background-repeat: no-repeat; background-position: right; padding-right: 20px; height: 20px; margin-left: 30px; } .naviSeparator { display: none } /* ----------- footer and license ----------- */ .footer { text-align: left; padding-top: 45px; padding-left: 5px; margin-top: 45px; margin-bottom: 45px; font-size: 10px; border-top: 1px solid #999; } .footer p { line-height: 14px; font-size: 11px; padding: 0; margin: 0; } .footer a[href*="http://"], a[href*="ftp://"], a[href*="https://"] { font-weight: bold; } .footerNavi { width: auto; text-align: right; margin-top: 50px; z-index: 1; } .navigationbar { display: block; position: relative; top: -20px; border-top: 1px solid #cecece; border-bottom: 1px solid #cecece; background-color: #F2F2F2; z-index: 1; height: 20px; padding-left: 7px; margin: 0px; padding-top: 2px; margin-left: -5px; margin-right: -5px; } .navigationbar .first { background: url(../images/home.png); background-position: left; background-repeat: no-repeat; padding-left: 20px; } .navigationbar ul { margin: 0px; padding: 0px; } .navigationbar ul li { list-style-type: none; padding-top: 2px; padding-left: 4px; margin: 0; height: 20px; } .navigationbar li { float: left } .navigationbar li a, .navigationbar td a { display: block; text-decoration: none; background: url(../images/arrow_bc.png); background-repeat: no-repeat; background-position: right; padding-right: 17px; } table.buildversion { float: right; margin-top: -18px !important; } .navigationbar table { border-radius: 0; border: 0 none; background-color: #F2F2F2; margin: 0; } .navigationbar table td { padding: 0; border: 0 none; } #buildversion { font-style: italic; font-size: small; float: right; margin-right: 5px; } /* /* table of content no display */ /* ----------- headers ----------- */ @media screen { .title { color: #313131; font-size: 24px; font-weight: normal; left: 0; padding-bottom: 20px; padding-left: 10px; padding-top: 20px; position: absolute; right: 0; top: 0; background-color: #E6E6E6; border-bottom: 1px #CCC solid; border-top: 2px #CCC solid; font-weight: bold; margin-left: 0px; margin-right: 0px; } .subtitle, .small-subtitle { display: block; clear: left; } } h1 { margin: 0 } h2, p.h2 { font: 500 16px/1.2 Arial; font-weight: 100; background-color: #F2F3F4; padding: 4px; margin-bottom: 30px; margin-top: 30px; border-top: #E0E0DE 1px solid; border-bottom: #E0E0DE 1px solid; max-width: 99%; } h2:target { background-color: #F2F3D4; } h3 { font: 500 14px/1.2 Arial; font-weight: 100; text-decoration: underline; margin-bottom: 30px; margin-top: 30px; } h3.fn, span.fn { border-width: 1px; border-style: solid; border-color: #E6E6E6; -moz-border-radius: 7px 7px 7px 7px; -webkit-border-radius: 7px 7px 7px 7px; border-radius: 7px 7px 7px 7px; background-color: #F6F6F6; word-spacing: 3px; padding: 5px 5px; text-decoration: none; font-weight: bold; max-width: 75%; font-size: 14px; margin: 0px; margin-top: 45px; } h3.fn code { float: right; } h3.fn:target { background-color: #F6F6D6; } .name { color: #1A1A1A } .type { color: #808080 } @media print { .title { color: #0066CB; font-family: Arial, Helvetica; font-size: 32px; font-weight: normal; left: 0; position: absolute; right: 0; top: 0; } } /* ----------------- table styles ----------------- */ .table img { border: none; margin-left: 0px; -moz-box-shadow: 0px 0px 0px #fff; -webkit-box-shadow: 0px 0px 0px #fff; box-shadow: 0px 0px 0px #fff; } /* table with border alternative colours*/ table, pre, .LegaleseLeft { -moz-border-radius: 7px 7px 7px 7px; -webkit-border-radius: 7px 7px 7px 7px; border-radius: 7px 7px 7px 7px; background-color: #F6F6F6; border: 1px solid #E6E6E6; border-collapse: separate; margin-bottom: 25px; margin-left: 15px; font-size: 12px; line-height: 1.2; } table tr.even { background-color: white; color: #66666E; } table tr.odd { background-color: #F6F6F6; color: #66666E; } table tr:target { background-color: #F6F6D6; } table thead { text-align: left; padding-left: 20px; background-color: #e1e0e0; border-left: none; border-right: none; } table thead th { padding-top: 5px; padding-left: 10px; padding-bottom: 5px; border-bottom: 2px solid #D1D1D1; padding-right: 10px; } table th { text-align: left; padding-left: 20px; } table td { padding: 3px 15px 3px 20px; border-bottom: #CCC dotted 1px; } table p { margin: 0px } .LegaleseLeft { font-family: monospace; white-space: pre-wrap; } /* table bodless & white*/ .borderless { border-radius: 0px 0px 0px 0px; background-color: #fff; border: 1px solid #fff; } .borderless tr { background-color: #FFF; color: #66666E; } .borderless td { border: none; border-bottom: #fff dotted 1px; } /* ----------- List ----------- */ ul { margin-top: 10px; } li { margin-bottom: 10px; padding-left: 8px; list-style: outside; text-align: left; } ul > li { list-style-type: square; } ol { margin: 10px; padding: 0; } ol.A > li { list-style-type: upper-alpha; } ol.a > li{ list-style-type: lower-alpha; } ol > li { margin-left: 30px; padding-left: 8px; list-style: decimal; } .centerAlign { text-align: left } .cpp, .LegaleseLeft { display: block; margin: 10px; overflow: auto; padding: 20px 20px 20px 20px; } .js { display: block; margin: 10px; overflow: auto; padding: 20px 20px 20px 20px; } .memItemLeft { padding-right: 3px } .memItemRight { padding: 3px 15px 3px 0 } .qml { display: block; margin: 10px; overflow: auto; padding: 20px 20px 20px 20px; } .qmldefault { padding-left: 5px; float: right; color: red; } .qmlreadonly { padding-left: 5px; float: right; color: #254117; } .rightAlign { padding: 3px 5px 3px 10px; text-align: right; } .qmldoc { margin-left: 15px } .flowList { padding: 25px } .flowList dd { display: inline-block; margin-left: 10px; width: 255px; line-height: 1.15em; overflow-x: hidden; text-overflow: ellipsis } .alphaChar { font-size: 2em; position: relative } /* ----------- Content table ----------- */ @media print { .toc { float: right; clear: right; padding-bottom: 10px; padding-top: 50px; width: 100%; background-image: url(../images/bgrContent.png); background-position: top; background-repeat: no-repeat; } } @media screen { .toc { float: right; clear: right; vertical-align: top; -moz-border-radius: 7px 7px 7px 7px; -webkit-border-radius: 7px 7px 7px 7px; border-radius: 7px 7px 7px 7px; background: #FFF url('../images/bgrContent.png'); background-position: top; background-repeat: repeat-x; border: 1px solid #E6E6E6; padding-left: 5px; padding-bottom: 10px; height: auto; width: 200px; text-align: left; margin-left: 20px; } } .toc h3 { text-decoration: none } .toc h3 { font: 500 14px/1.2 Arial; font-weight: 100; padding: 0px; margin: 0px; padding-top: 5px; padding-left: 5px; } .toc ul { padding-left: 10px; padding-right: 5px; } .toc ul li { margin-left: 15px; list-style-image: url(../images/bullet_dn.png); marker-offset: 0px; margin-bottom: 8px; padding-left: 0px; } .toc .level1 { border: none } .toc .level2 { border: none; margin-left: 25px; } .level3 { border: none; margin-left: 30px; } .clearfix { clear: both } /* ----------- Landing page ----------- */ .col-group { white-space: nowrap; vertical-align: top; } .landing h2 { background-color: transparent; border: none; margin-bottom: 0px; font-size: 18px; } .landing a, .landing li { font-size: 13px; font-weight: bold !important; } .col-1 { display: inline-block; white-space: normal; width: 70%; height: 100%; float: left; } .col-2 { display: inline-block; white-space: normal; width: 20%; margin-left: 5%; position: relative; top: -20px; } .col-1 h1 { margin: 20px 0 0 0; } .col-1 h2 { font-size: 18px; font-weight: bold !important; } .landingicons { display: inline-block; width: 100%; } .icons1of3 { display: inline-block; width: 33.3333%; float: left; } .icons1of3 h2, .doc-column h2 { font-size: 15px; margin: 0px; padding: 0px; } div.multi-column { position: relative; } div.multi-column div { display: -moz-inline-box; display: inline-block; vertical-align: top; margin-top: 1em; margin-right: 4em; width: 24em; } spectral/include/SortFilterProxyModel/docs/qml-filter-members.html0000644000175000000620000000143013566674121025446 0ustar dilingerstaff List of All Members for Filter | SortFilterProxyModel

List of All Members for Filter

This is the complete list of members for Filter, including inherited members.

spectral/include/SortFilterProxyModel/docs/sortfilterproxymodel.index0000644000175000000620000006547113566674121026444 0ustar dilingerstaff spectral/include/SortFilterProxyModel/docs/qml-switchrole.html0000644000175000000620000001420613566674121024721 0ustar dilingerstaff SwitchRole QML Type | SortFilterProxyModel

SwitchRole QML Type

A role using Filter to conditionnaly compute its data More...

Import Statement: import .
Inherits:

SingleRole

Properties

Attached Properties

Detailed Description

A SwitchRole is a ProxyRole that computes its data with the help of Filter. Each top level filters specified in the SwitchRole is evaluated on the rows of the model, if a Filter evaluates to true, the data of the SwitchRole for this row will be the one of the attached SwitchRole.value property. If no top level filters evaluate to true, the data will default to the one of the defaultRoleName (or the defaultValue if no defaultRoleName is specified).

In the following example, the favoriteOrFirstNameSection role is equal to * if the favorite role of a row is true, otherwise it's the same as the firstName role :

SortFilterProxyModel {
   sourceModel: contactModel
   proxyRoles: SwitchRole {
       name: "favoriteOrFirstNameSection"
       filters: ValueFilter {
           roleName: "favorite"
           value: true
           SwitchRole.value: "*"
       }
       defaultRoleName: "firstName"
    }
}

Property Documentation

defaultRoleName : string

This property holds the default role name of the role. If no filter match a row, the data of this role will be the data of the role whose name is defaultRoleName.


defaultValue : var

This property holds the default value of the role. If no filter match a row, and no defaultRoleName is set, the data of this role will be defaultValue.


filters : list<Filter>

This property holds the list of filters for this proxy role. The data of this role will be equal to the attached SwitchRole.value property of the first filter that matches the model row.

See also Filter.


Attached Property Documentation

SwitchRole.value : var

This property attaches a value to a Filter.


spectral/include/SortFilterProxyModel/docs/qml-proxyrole.html0000644000175000000620000000337313566674121024604 0ustar dilingerstaff ProxyRole QML Type | SortFilterProxyModel

ProxyRole QML Type

Base type for the SortFilterProxyModel proxy roles More...

Import Statement: import .
Inherited By:

RegExpRole and SingleRole

Detailed Description

The ProxyRole type cannot be used directly in a QML file. It exists to provide a set of common properties and methods, available across all the other proxy role types that inherit from it. Attempting to use the ProxyRole type directly will result in an error.

spectral/include/SortFilterProxyModel/docs/qml-sorter-members.html0000644000175000000620000000144313566674121025503 0ustar dilingerstaff List of All Members for Sorter | SortFilterProxyModel

List of All Members for Sorter

This is the complete list of members for Sorter, including inherited members.

spectral/include/SortFilterProxyModel/docs/qml-expressionfilter.html0000644000175000000620000000657013566674121026150 0ustar dilingerstaff ExpressionFilter QML Type | SortFilterProxyModel

ExpressionFilter QML Type

Filters row with a custom filtering More...

Import Statement: import .
Inherits:

Filter

Properties

Detailed Description

An ExpressionFilter is a Filter allowing to implement custom filtering based on a javascript expression.

Property Documentation

expression : expression

An expression to implement custom filtering, it must evaluate to a boolean. It has the same syntax has a Property Binding except it will be evaluated for each of the source model's rows. Rows that have their expression evaluating to true will be accepted by the model. Data for each row is exposed like for a delegate of a QML View.

This expression is reevaluated for a row every time its model data changes. When an external property (not index or in model) the expression depends on changes, the expression is reevaluated for every row of the source model. To capture the properties the expression depends on, the expression is first executed with invalid data and each property access is detected by the QML engine. This means that if a property is not accessed because of a conditional, it won't be captured and the expression won't be reevaluted when this property changes.

A workaround to this problem is to access all the properties the expressions depends unconditionally at the beggining of the expression.


spectral/include/SortFilterProxyModel/docs/qml-expressionrole-members.html0000644000175000000620000000167313566674121027253 0ustar dilingerstaff List of All Members for ExpressionRole | SortFilterProxyModel

List of All Members for ExpressionRole

This is the complete list of members for ExpressionRole, including inherited members.

The following members are inherited from SingleRole.

spectral/include/SortFilterProxyModel/docs/qml-regexprole-members.html0000644000175000000620000000167113566674121026344 0ustar dilingerstaff List of All Members for RegExpRole | SortFilterProxyModel

List of All Members for RegExpRole

This is the complete list of members for RegExpRole, including inherited members.

spectral/include/SortFilterProxyModel/docs/qml-stringsorter-members.html0000644000175000000620000000305713566674121026735 0ustar dilingerstaff List of All Members for StringSorter | SortFilterProxyModel

List of All Members for StringSorter

This is the complete list of members for StringSorter, including inherited members.

The following members are inherited from RoleSorter.

The following members are inherited from Sorter.

spectral/include/SortFilterProxyModel/docs/qml-joinrole.html0000644000175000000620000000663013566674121024361 0ustar dilingerstaff JoinRole QML Type | SortFilterProxyModel

JoinRole QML Type

a role made from concatenating other roles More...

Import Statement: import .
Inherits:

SingleRole

Properties

Detailed Description

A JoinRole is a simple ProxyRole that concatenates other roles.

In the following example, the fullName role is computed by the concatenation of the firstName role and the lastName role separated by a space :

SortFilterProxyModel {
   sourceModel: contactModel
   proxyRoles: JoinRole {
       name: "fullName"
       roleNames: ["firstName", "lastName"]
  }
}

Property Documentation

roleNames : list<string>

This property holds the role names that are joined by this role.


separator : string

This property holds the separator that is used to join the roles specified in roleNames.

By default, it's a space.


spectral/include/SortFilterProxyModel/docs/qml-rolesorter-members.html0000644000175000000620000000177413566674121026374 0ustar dilingerstaff List of All Members for RoleSorter | SortFilterProxyModel

List of All Members for RoleSorter

This is the complete list of members for RoleSorter, including inherited members.

The following members are inherited from Sorter.

spectral/include/SortFilterProxyModel/docs/qml-filter.html0000644000175000000620000000633113566674121024023 0ustar dilingerstaff Filter QML Type | SortFilterProxyModel

Filter QML Type

Base type for the SortFilterProxyModel filters More...

Import Statement: import .
Inherited By:

AllOf, AnyOf, ExpressionFilter, IndexFilter, and RoleFilter

Properties

Detailed Description

The Filter type cannot be used directly in a QML file. It exists to provide a set of common properties and methods, available across all the other filter types that inherit from it. Attempting to use the Filter type directly will result in an error.

Property Documentation

enabled : bool

This property holds whether the filter is enabled. A disabled filter will accept every rows unconditionally (even if it's inverted).

By default, filters are enabled.


inverted : bool

This property holds whether the filter is inverted. When a filter is inverted, a row normally accepted would be rejected, and vice-versa.

By default, filters are not inverted.


spectral/include/SortFilterProxyModel/docs/index.html0000644000175000000620000001162313566674121023056 0ustar dilingerstaff SortFilterProxyModel QML Module | SortFilterProxyModel
  • SortFilterProxyModel QML Module
  • SortFilterProxyModel QML Module

    SortFilterProxyModel is an implementation of QSortFilterProxyModel conveniently exposed for QML.

    AllOf

    Filter container accepting rows accepted by all its child filters

    AnyOf

    Filter container accepting rows accepted by at least one of its child filters

    ExpressionFilter

    Filters row with a custom filtering

    Filter

    Base type for the SortFilterProxyModel filters

    IndexFilter

    Filters rows based on their source index

    RangeFilter

    Filters rows between boundary values

    RegExpFilter

    Filters rows matching a regular expression

    RoleFilter

    Base type for filters based on a source model role

    ValueFilter

    Filters rows matching exactly a value

    ExpressionRole

    A custom role computed from a javascrip expression

    FilterRole

    A role resolving to true for rows matching all its filters

    JoinRole

    Role made from concatenating other roles

    ProxyRole

    Base type for the SortFilterProxyModel proxy roles

    RegExpRole

    A ProxyRole extracting data from a source role via a regular expression

    SingleRole

    Base type for the SortFilterProxyModel proxy roles defining a single role

    SwitchRole

    A role using Filter to conditionnaly compute its data

    SortFilterProxyModel

    Filters and sorts data coming from a source QAbstractItemModel

    ExpressionSorter

    Sorts row with a custom sorting

    FilterSorter

    Sorts rows based on if they match filters

    RoleSorter

    Sorts rows based on a source model role

    Sorter

    Base type for the SortFilterProxyModel sorters

    StringSorter

    Sorts rows based on a source model string role

    spectral/include/SortFilterProxyModel/docs/qml-rangefilter-members.html0000644000175000000620000000265713566674121026477 0ustar dilingerstaff List of All Members for RangeFilter | SortFilterProxyModel

    List of All Members for RangeFilter

    This is the complete list of members for RangeFilter, including inherited members.

    The following members are inherited from Filter.

    spectral/include/SortFilterProxyModel/docs/qml-singlerole-members.html0000644000175000000620000000131613566674121026327 0ustar dilingerstaff List of All Members for SingleRole | SortFilterProxyModel

    List of All Members for SingleRole

    This is the complete list of members for SingleRole, including inherited members.

    spectral/include/SortFilterProxyModel/docs/qml-rolefilter-members.html0000644000175000000620000000176113566674121026337 0ustar dilingerstaff List of All Members for RoleFilter | SortFilterProxyModel

    List of All Members for RoleFilter

    This is the complete list of members for RoleFilter, including inherited members.

    The following members are inherited from Filter.

    spectral/include/SortFilterProxyModel/docs/qml-sorter.html0000644000175000000620000000713213566674121024054 0ustar dilingerstaff Sorter QML Type | SortFilterProxyModel

    Sorter QML Type

    Base type for the SortFilterProxyModel sorters More...

    Import Statement: import .
    Inherited By:

    ExpressionSorter, FilterSorter, and RoleSorter

    Properties

    Detailed Description

    The Sorter type cannot be used directly in a QML file. It exists to provide a set of common properties and methods, available across all the other sorters types that inherit from it. Attempting to use the Sorter type directly will result in an error.

    Property Documentation

    enabled : bool

    This property holds whether the sorter is enabled. A disabled sorter will not change the order of the rows.

    By default, sorters are enabled.


    sortOrder : Qt::SortOrder

    This property holds the sort order of this sorter.

    ConstantDescription
    Qt.AscendingOrderThe items are sorted ascending e.g. starts with 'AAA' ends with 'ZZZ' in Latin-1 locales
    Qt.DescendingOrderThe items are sorted descending e.g. starts with 'ZZZ' ends with 'AAA' in Latin-1 locales

    By default, sorting is in ascending order.


    spectral/include/SortFilterProxyModel/docs/qml-indexfilter-members.html0000644000175000000620000000214613566674121026503 0ustar dilingerstaff List of All Members for IndexFilter | SortFilterProxyModel

    List of All Members for IndexFilter

    This is the complete list of members for IndexFilter, including inherited members.

    The following members are inherited from Filter.

    spectral/include/SortFilterProxyModel/docs/qml-expressionfilter-members.html0000644000175000000620000000203513566674121027570 0ustar dilingerstaff List of All Members for ExpressionFilter | SortFilterProxyModel

    List of All Members for ExpressionFilter

    This is the complete list of members for ExpressionFilter, including inherited members.

    The following members are inherited from Filter.

    spectral/include/SortFilterProxyModel/docs/qml-valuefilter-members.html0000644000175000000620000000212713566674121026507 0ustar dilingerstaff List of All Members for ValueFilter | SortFilterProxyModel

    List of All Members for ValueFilter

    This is the complete list of members for ValueFilter, including inherited members.

    The following members are inherited from Filter.

    spectral/include/SortFilterProxyModel/docs/qml-joinrole-members.html0000644000175000000620000000200113566674121025775 0ustar dilingerstaff List of All Members for JoinRole | SortFilterProxyModel

    List of All Members for JoinRole

    This is the complete list of members for JoinRole, including inherited members.

    The following members are inherited from SingleRole.

    spectral/include/SortFilterProxyModel/docs/qml-filterrole-members.html0000644000175000000620000000163113566674121026333 0ustar dilingerstaff List of All Members for FilterRole | SortFilterProxyModel

    List of All Members for FilterRole

    This is the complete list of members for FilterRole, including inherited members.

    The following members are inherited from SingleRole.

    spectral/include/SortFilterProxyModel/docs/qml-rolesorter.html0000644000175000000620000000517513566674121024743 0ustar dilingerstaff RoleSorter QML Type | SortFilterProxyModel

    RoleSorter QML Type

    Sorts rows based on a source model role More...

    Import Statement: import .
    Inherits:

    Sorter

    Inherited By:

    StringSorter

    Properties

    Detailed Description

    A RoleSorter is a simple Sorter that sorts rows based on a source model role.

    In the following example, rows with be sorted by their lastName role :

    SortFilterProxyModel {
       sourceModel: contactModel
       sorters: RoleSorter { roleName: "lastName" }
    }

    Property Documentation

    roleName : string

    This property holds the role name that the sorter is using to query the source model's data when sorting items.


    spectral/include/SortFilterProxyModel/docs/qml-expressionsorter-members.html0000644000175000000620000000205013566674121027616 0ustar dilingerstaff List of All Members for ExpressionSorter | SortFilterProxyModel

    List of All Members for ExpressionSorter

    This is the complete list of members for ExpressionSorter, including inherited members.

    The following members are inherited from Sorter.

    spectral/include/SortFilterProxyModel/docs/qml-filterrole.html0000644000175000000620000000562013566674121024705 0ustar dilingerstaff FilterRole QML Type | SortFilterProxyModel

    FilterRole QML Type

    A role resolving to true for rows matching all its filters More...

    Import Statement: import .
    Inherits:

    SingleRole

    Properties

    Detailed Description

    A FilterRole is a ProxyRole that returns true for rows matching all its filters.

    In the following example, the isAdult role will be equal to true if the age role is superior or equal to 18.

    SortFilterProxyModel {
        sourceModel: personModel
        proxyRoles: FilterRole {
            name: "isAdult"
            RangeFilter { roleName: "age"; minimumValue: 18; minimumInclusive: true }
        }
    }

    Property Documentation

    filters : string

    This property holds the list of filters for this filter role. The data of this role will be equal to the true if all its filters match the model row, false otherwise.

    See also Filter.


    spectral/include/SortFilterProxyModel/docs/qml-regexpfilter-members.html0000644000175000000620000000250013566674121026660 0ustar dilingerstaff List of All Members for RegExpFilter | SortFilterProxyModel

    List of All Members for RegExpFilter

    This is the complete list of members for RegExpFilter, including inherited members.

    The following members are inherited from Filter.

    spectral/include/SortFilterProxyModel/docs/qml-expressionsorter.html0000644000175000000620000000752413566674121026201 0ustar dilingerstaff ExpressionSorter QML Type | SortFilterProxyModel

    ExpressionSorter QML Type

    Sorts row with a custom sorting More...

    Import Statement: import .
    Inherits:

    Sorter

    Properties

    Detailed Description

    An ExpressionSorter is a Sorter allowing to implement custom sorting based on a javascript expression.

    Property Documentation

    expression : expression

    An expression to implement custom sorting. It must evaluate to a bool. It has the same syntax has a Property Binding, except that it will be evaluated for each of the source model's rows. Model data is accessible for both rows with the modelLeft, and modelRight properties:

    sorters: ExpressionSorter {
        expression: {
            return modelLeft.someRole < modelRight.someRole;
        }
    }

    The index of the row is also available through modelLeft and modelRight.

    The expression should return true if the value of the left item is less than the value of the right item, otherwise returns false.

    This expression is reevaluated for a row every time its model data changes. When an external property (not index* or in model*) the expression depends on changes, the expression is reevaluated for every row of the source model. To capture the properties the expression depends on, the expression is first executed with invalid data and each property access is detected by the QML engine. This means that if a property is not accessed because of a conditional, it won't be captured and the expression won't be reevaluted when this property changes.

    A workaround to this problem is to access all the properties the expressions depends unconditionally at the beggining of the expression.


    spectral/include/SortFilterProxyModel/docs/qml-indexfilter.html0000644000175000000620000000767513566674121025067 0ustar dilingerstaff IndexFilter QML Type | SortFilterProxyModel

    IndexFilter QML Type

    Filters rows based on their source index More...

    Import Statement: import .
    Inherits:

    Filter

    Properties

    Detailed Description

    An IndexFilter is a filter allowing contents to be filtered based on their source model index.

    In the following example, only the first row of the source model will be accepted:

    SortFilterProxyModel {
       sourceModel: contactModel
       filters: IndexFilter {
           maximumIndex: 0
       }
    }

    Property Documentation

    maximumIndex : int

    This property holds the maximumIndex of the filter. Rows with a source index higher than maximumIndex will be rejected.

    If maximumIndex is negative, it is counted from the end of the source model, meaning that:

    maximumIndex: -1

    is equivalent to :

    maximumIndex: sourceModel.count - 1

    By default, no value is set.


    minimumIndex : int

    This property holds the minimumIndex of the filter. Rows with a source index lower than minimumIndex will be rejected.

    If minimumIndex is negative, it is counted from the end of the source model, meaning that :

    minimumIndex: -1

    is equivalent to :

    minimumIndex: sourceModel.count - 1

    By default, no value is set.


    spectral/include/SortFilterProxyModel/SortFilterProxyModel.pri0000644000175000000620000000422613566674121024766 0ustar dilingerstaff!contains( CONFIG, c\+\+1[14] ): warning("SortFilterProxyModel needs at least c++11, add CONFIG += c++11 to your .pro") INCLUDEPATH += $$PWD HEADERS += $$PWD/qqmlsortfilterproxymodel.h \ $$PWD/filters/filter.h \ $$PWD/filters/filtercontainer.h \ $$PWD/filters/rolefilter.h \ $$PWD/filters/valuefilter.h \ $$PWD/filters/indexfilter.h \ $$PWD/filters/regexpfilter.h \ $$PWD/filters/rangefilter.h \ $$PWD/filters/expressionfilter.h \ $$PWD/filters/filtercontainerfilter.h \ $$PWD/filters/anyoffilter.h \ $$PWD/filters/alloffilter.h \ $$PWD/sorters/sorter.h \ $$PWD/sorters/sortercontainer.h \ $$PWD/sorters/rolesorter.h \ $$PWD/sorters/stringsorter.h \ $$PWD/sorters/expressionsorter.h \ $$PWD/proxyroles/proxyrole.h \ $$PWD/proxyroles/proxyrolecontainer.h \ $$PWD/proxyroles/joinrole.h \ $$PWD/proxyroles/switchrole.h \ $$PWD/proxyroles/expressionrole.h \ $$PWD/proxyroles/singlerole.h \ $$PWD/proxyroles/regexprole.h \ $$PWD/sorters/filtersorter.h \ $$PWD/proxyroles/filterrole.h SOURCES += $$PWD/qqmlsortfilterproxymodel.cpp \ $$PWD/filters/filter.cpp \ $$PWD/filters/filtercontainer.cpp \ $$PWD/filters/rolefilter.cpp \ $$PWD/filters/valuefilter.cpp \ $$PWD/filters/indexfilter.cpp \ $$PWD/filters/regexpfilter.cpp \ $$PWD/filters/rangefilter.cpp \ $$PWD/filters/expressionfilter.cpp \ $$PWD/filters/filtercontainerfilter.cpp \ $$PWD/filters/anyoffilter.cpp \ $$PWD/filters/alloffilter.cpp \ $$PWD/filters/filtersqmltypes.cpp \ $$PWD/sorters/sorter.cpp \ $$PWD/sorters/sortercontainer.cpp \ $$PWD/sorters/rolesorter.cpp \ $$PWD/sorters/stringsorter.cpp \ $$PWD/sorters/expressionsorter.cpp \ $$PWD/sorters/sortersqmltypes.cpp \ $$PWD/proxyroles/proxyrole.cpp \ $$PWD/proxyroles/proxyrolecontainer.cpp \ $$PWD/proxyroles/joinrole.cpp \ $$PWD/proxyroles/switchrole.cpp \ $$PWD/proxyroles/expressionrole.cpp \ $$PWD/proxyroles/proxyrolesqmltypes.cpp \ $$PWD/proxyroles/singlerole.cpp \ $$PWD/proxyroles/regexprole.cpp \ $$PWD/sorters/filtersorter.cpp \ $$PWD/proxyroles/filterrole.cpp spectral/include/SortFilterProxyModel/.git0000644000175000000620000000007013566674120020706 0ustar dilingerstaffgitdir: ../../.git/modules/include/SortFilterProxyModel spectral/include/SortFilterProxyModel/qpm.json0000644000175000000620000000076213566674121021623 0ustar dilingerstaff{ "name": "fr.grecko.sortfilterproxymodel", "description": "A nicely exposed QSortFilterProxyModel for QML", "author": { "name": "Pierre-Yves Siret", "email": "gr3cko@gmail.com" }, "repository": { "type": "GITHUB", "url": "https://github.com/oKcerG/SortFilterProxyModel.git" }, "version": { "label": "0.1.0", "revision": "", "fingerprint": "" }, "dependencies": [ ], "license": "MIT", "pri_filename": "SortFilterProxyModel.pri", "webpage": "" }spectral/include/SortFilterProxyModel/qqmlsortfilterproxymodel.h0000644000175000000620000001232213566674121025512 0ustar dilingerstaff#ifndef QQMLSORTFILTERPROXYMODEL_H #define QQMLSORTFILTERPROXYMODEL_H #include #include #include "filters/filtercontainer.h" #include "sorters/sortercontainer.h" #include "proxyroles/proxyrolecontainer.h" namespace qqsfpm { class QQmlSortFilterProxyModel : public QSortFilterProxyModel, public QQmlParserStatus, public FilterContainer, public SorterContainer, public ProxyRoleContainer { Q_OBJECT Q_INTERFACES(QQmlParserStatus) Q_INTERFACES(qqsfpm::FilterContainer) Q_INTERFACES(qqsfpm::SorterContainer) Q_INTERFACES(qqsfpm::ProxyRoleContainer) Q_PROPERTY(int count READ count NOTIFY countChanged) Q_PROPERTY(QString filterRoleName READ filterRoleName WRITE setFilterRoleName NOTIFY filterRoleNameChanged) Q_PROPERTY(QString filterPattern READ filterPattern WRITE setFilterPattern NOTIFY filterPatternChanged) Q_PROPERTY(PatternSyntax filterPatternSyntax READ filterPatternSyntax WRITE setFilterPatternSyntax NOTIFY filterPatternSyntaxChanged) Q_PROPERTY(QVariant filterValue READ filterValue WRITE setFilterValue NOTIFY filterValueChanged) Q_PROPERTY(QString sortRoleName READ sortRoleName WRITE setSortRoleName NOTIFY sortRoleNameChanged) Q_PROPERTY(bool ascendingSortOrder READ ascendingSortOrder WRITE setAscendingSortOrder NOTIFY ascendingSortOrderChanged) Q_PROPERTY(QQmlListProperty filters READ filtersListProperty) Q_PROPERTY(QQmlListProperty sorters READ sortersListProperty) Q_PROPERTY(QQmlListProperty proxyRoles READ proxyRolesListProperty) public: enum PatternSyntax { RegExp = QRegExp::RegExp, Wildcard = QRegExp::Wildcard, FixedString = QRegExp::FixedString, RegExp2 = QRegExp::RegExp2, WildcardUnix = QRegExp::WildcardUnix, W3CXmlSchema11 = QRegExp::W3CXmlSchema11 }; Q_ENUMS(PatternSyntax) QQmlSortFilterProxyModel(QObject* parent = 0); int count() const; const QString& filterRoleName() const; void setFilterRoleName(const QString& filterRoleName); QString filterPattern() const; void setFilterPattern(const QString& filterPattern); PatternSyntax filterPatternSyntax() const; void setFilterPatternSyntax(PatternSyntax patternSyntax); const QVariant& filterValue() const; void setFilterValue(const QVariant& filterValue); const QString& sortRoleName() const; void setSortRoleName(const QString& sortRoleName); bool ascendingSortOrder() const; void setAscendingSortOrder(bool ascendingSortOrder); void classBegin() override; void componentComplete() override; QVariant sourceData(const QModelIndex& sourceIndex, const QString& roleName) const; QVariant sourceData(const QModelIndex& sourceIndex, int role) const; QVariant data(const QModelIndex& index, int role) const override; QHash roleNames() const override; Q_INVOKABLE int roleForName(const QString& roleName) const; Q_INVOKABLE QVariantMap get(int row) const; Q_INVOKABLE QVariant get(int row, const QString& roleName) const; Q_INVOKABLE QModelIndex mapToSource(const QModelIndex& proxyIndex) const override; Q_INVOKABLE int mapToSource(int proxyRow) const; Q_INVOKABLE QModelIndex mapFromSource(const QModelIndex& sourceIndex) const override; Q_INVOKABLE int mapFromSource(int sourceRow) const; void setSourceModel(QAbstractItemModel *sourceModel) override; Q_SIGNALS: void countChanged(); void filterRoleNameChanged(); void filterPatternSyntaxChanged(); void filterPatternChanged(); void filterValueChanged(); void sortRoleNameChanged(); void ascendingSortOrderChanged(); protected: bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; protected Q_SLOTS: void resetInternalData(); private Q_SLOTS: void invalidateFilter(); void invalidate(); void updateRoleNames(); void updateFilterRole(); void updateSortRole(); void updateRoles(); void initRoles(); void onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles); void emitProxyRolesChanged(); private: QVariantMap modelDataMap(const QModelIndex& modelIndex) const; void onFilterAppended(Filter* filter) override; void onFilterRemoved(Filter* filter) override; void onFiltersCleared() override; void onSorterAppended(Sorter* sorter) override; void onSorterRemoved(Sorter* sorter) override; void onSortersCleared() override; void onProxyRoleAppended(ProxyRole *proxyRole) override; void onProxyRoleRemoved(ProxyRole *proxyRole) override; void onProxyRolesCleared() override; QString m_filterRoleName; QVariant m_filterValue; QString m_sortRoleName; bool m_ascendingSortOrder = true; bool m_completed = false; QHash m_roleNames; QHash> m_proxyRoleMap; QVector m_proxyRoleNumbers; }; } #endif // QQMLSORTFILTERPROXYMODEL_H spectral/include/SortFilterProxyModel/sorters/0002755000175000000620000000000013566674121021631 5ustar dilingerstaffspectral/include/SortFilterProxyModel/sorters/stringsorter.cpp0000644000175000000620000000606013566674121025102 0ustar dilingerstaff#include "stringsorter.h" namespace qqsfpm { /*! \qmltype StringSorter \inherits RoleSorter \inqmlmodule SortFilterProxyModel \brief Sorts rows based on a source model string role \l StringSorter is a specialized \l RoleSorter that sorts rows based on a source model string role. \l StringSorter compares strings according to a localized collation algorithm. In the following example, rows with be sorted by their \c lastName role : \code SortFilterProxyModel { sourceModel: contactModel sorters: StringSorter { roleName: "lastName" } } \endcode */ /*! \qmlproperty Qt.CaseSensitivity StringSorter::caseSensitivity This property holds the case sensitivity of the sorter. */ Qt::CaseSensitivity StringSorter::caseSensitivity() const { return m_collator.caseSensitivity(); } void StringSorter::setCaseSensitivity(Qt::CaseSensitivity caseSensitivity) { if (m_collator.caseSensitivity() == caseSensitivity) return; m_collator.setCaseSensitivity(caseSensitivity); Q_EMIT caseSensitivityChanged(); invalidate(); } /*! \qmlproperty bool StringSorter::ignorePunctation This property holds whether the sorter ignores punctation. if \c ignorePunctuation is \c true, punctuation characters and symbols are ignored when determining sort order. \note This property is not currently supported on Apple platforms or if Qt is configured to not use ICU on Linux. */ bool StringSorter::ignorePunctation() const { return m_collator.ignorePunctuation(); } void StringSorter::setIgnorePunctation(bool ignorePunctation) { if (m_collator.ignorePunctuation() == ignorePunctation) return; m_collator.setIgnorePunctuation(ignorePunctation); Q_EMIT ignorePunctationChanged(); invalidate(); } /*! \qmlproperty Locale StringSorter::locale This property holds the locale of the sorter. */ QLocale StringSorter::locale() const { return m_collator.locale(); } void StringSorter::setLocale(const QLocale &locale) { if (m_collator.locale() == locale) return; m_collator.setLocale(locale); Q_EMIT localeChanged(); invalidate(); } /*! \qmlproperty bool StringSorter::numericMode This property holds whether the numeric mode of the sorter is enabled. This will enable proper sorting of numeric digits, so that e.g. 100 sorts after 99. By default this mode is off. */ bool StringSorter::numericMode() const { return m_collator.numericMode(); } void StringSorter::setNumericMode(bool numericMode) { if (m_collator.numericMode() == numericMode) return; m_collator.setNumericMode(numericMode); Q_EMIT numericModeChanged(); invalidate(); } int StringSorter::compare(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const QQmlSortFilterProxyModel& proxyModel) const { QPair pair = sourceData(sourceLeft, sourceRight, proxyModel); QString leftValue = pair.first.toString(); QString rightValue = pair.second.toString(); return m_collator.compare(leftValue, rightValue); } } spectral/include/SortFilterProxyModel/sorters/sortercontainer.h0000644000175000000620000000211613566674121025221 0ustar dilingerstaff#ifndef SORTERSSORTERCONTAINER_H #define SORTERSSORTERCONTAINER_H #include #include namespace qqsfpm { class Sorter; class QQmlSortFilterProxyModel; class SorterContainer { public: virtual ~SorterContainer(); QList sorters() const; void appendSorter(Sorter* sorter); void removeSorter(Sorter* sorter); void clearSorters(); QQmlListProperty sortersListProperty(); protected: QList m_sorters; private: virtual void onSorterAppended(Sorter* sorter) = 0; virtual void onSorterRemoved(Sorter* sorter) = 0; virtual void onSortersCleared() = 0; static void append_sorter(QQmlListProperty* list, Sorter* sorter); static int count_sorter(QQmlListProperty* list); static Sorter* at_sorter(QQmlListProperty* list, int index); static void clear_sorters(QQmlListProperty* list); }; } #define SorterContainer_iid "fr.grecko.SortFilterProxyModel.SorterContainer" Q_DECLARE_INTERFACE(qqsfpm::SorterContainer, SorterContainer_iid) #endif // SORTERSSORTERCONTAINER_H spectral/include/SortFilterProxyModel/sorters/sortercontainer.cpp0000644000175000000620000000322313566674121025554 0ustar dilingerstaff#include "sortercontainer.h" namespace qqsfpm { SorterContainer::~SorterContainer() { } QList SorterContainer::sorters() const { return m_sorters; } void SorterContainer::appendSorter(Sorter* sorter) { m_sorters.append(sorter); onSorterAppended(sorter); } void SorterContainer::removeSorter(Sorter *sorter) { m_sorters.removeOne(sorter); onSorterRemoved(sorter); } void SorterContainer::clearSorters() { m_sorters.clear(); onSortersCleared(); } QQmlListProperty SorterContainer::sortersListProperty() { return QQmlListProperty(reinterpret_cast(this), &m_sorters, &SorterContainer::append_sorter, &SorterContainer::count_sorter, &SorterContainer::at_sorter, &SorterContainer::clear_sorters); } void SorterContainer::append_sorter(QQmlListProperty* list, Sorter* sorter) { if (!sorter) return; SorterContainer* that = reinterpret_cast(list->object); that->appendSorter(sorter); } int SorterContainer::count_sorter(QQmlListProperty* list) { QList* sorters = static_cast*>(list->data); return sorters->count(); } Sorter* SorterContainer::at_sorter(QQmlListProperty* list, int index) { QList* sorters = static_cast*>(list->data); return sorters->at(index); } void SorterContainer::clear_sorters(QQmlListProperty *list) { SorterContainer* that = reinterpret_cast(list->object); that->clearSorters(); } } spectral/include/SortFilterProxyModel/sorters/rolesorter.cpp0000644000175000000620000000342013566674121024532 0ustar dilingerstaff#include "rolesorter.h" #include "qqmlsortfilterproxymodel.h" namespace qqsfpm { /*! \qmltype RoleSorter \inherits Sorter \inqmlmodule SortFilterProxyModel \brief Sorts rows based on a source model role A RoleSorter is a simple \l Sorter that sorts rows based on a source model role. In the following example, rows with be sorted by their \c lastName role : \code SortFilterProxyModel { sourceModel: contactModel sorters: RoleSorter { roleName: "lastName" } } \endcode */ /*! \qmlproperty string RoleSorter::roleName This property holds the role name that the sorter is using to query the source model's data when sorting items. */ const QString& RoleSorter::roleName() const { return m_roleName; } void RoleSorter::setRoleName(const QString& roleName) { if (m_roleName == roleName) return; m_roleName = roleName; Q_EMIT roleNameChanged(); invalidate(); } QPair RoleSorter::sourceData(const QModelIndex &sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const { QPair pair; int role = proxyModel.roleForName(m_roleName); if (role == -1) return pair; pair.first = proxyModel.sourceData(sourceLeft, role); pair.second = proxyModel.sourceData(sourceRight, role); return pair; } int RoleSorter::compare(const QModelIndex &sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const { QPair pair = sourceData(sourceLeft, sourceRight, proxyModel); QVariant leftValue = pair.first; QVariant rightValue = pair.second; if (leftValue < rightValue) return -1; if (leftValue > rightValue) return 1; return 0; } } spectral/include/SortFilterProxyModel/sorters/sorter.cpp0000644000175000000620000000546613566674121023664 0ustar dilingerstaff#include "sorter.h" #include "qqmlsortfilterproxymodel.h" namespace qqsfpm { /*! \qmltype Sorter \inqmlmodule SortFilterProxyModel \brief Base type for the \l SortFilterProxyModel sorters The Sorter type cannot be used directly in a QML file. It exists to provide a set of common properties and methods, available across all the other sorters types that inherit from it. Attempting to use the Sorter type directly will result in an error. */ Sorter::Sorter(QObject *parent) : QObject(parent) { } Sorter::~Sorter() = default; /*! \qmlproperty bool Sorter::enabled This property holds whether the sorter is enabled. A disabled sorter will not change the order of the rows. By default, sorters are enabled. */ bool Sorter::enabled() const { return m_enabled; } void Sorter::setEnabled(bool enabled) { if (m_enabled == enabled) return; m_enabled = enabled; Q_EMIT enabledChanged(); Q_EMIT invalidated(); } bool Sorter::ascendingOrder() const { return sortOrder() == Qt::AscendingOrder; } void Sorter::setAscendingOrder(bool ascendingOrder) { setSortOrder(ascendingOrder ? Qt::AscendingOrder : Qt::DescendingOrder); } /*! \qmlproperty Qt::SortOrder Sorter::sortOrder This property holds the sort order of this sorter. \value Qt.AscendingOrder The items are sorted ascending e.g. starts with 'AAA' ends with 'ZZZ' in Latin-1 locales \value Qt.DescendingOrder The items are sorted descending e.g. starts with 'ZZZ' ends with 'AAA' in Latin-1 locales By default, sorting is in ascending order. */ Qt::SortOrder Sorter::sortOrder() const { return m_sortOrder; } void Sorter::setSortOrder(Qt::SortOrder sortOrder) { if (m_sortOrder == sortOrder) return; m_sortOrder = sortOrder; Q_EMIT sortOrderChanged(); invalidate(); } int Sorter::compareRows(const QModelIndex &source_left, const QModelIndex &source_right, const QQmlSortFilterProxyModel& proxyModel) const { int comparison = compare(source_left, source_right, proxyModel); return (m_sortOrder == Qt::AscendingOrder) ? comparison : -comparison; } int Sorter::compare(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const QQmlSortFilterProxyModel& proxyModel) const { if (lessThan(sourceLeft, sourceRight, proxyModel)) return -1; if (lessThan(sourceRight, sourceLeft, proxyModel)) return 1; return 0; } void Sorter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) { Q_UNUSED(proxyModel) } bool Sorter::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const QQmlSortFilterProxyModel& proxyModel) const { Q_UNUSED(sourceLeft) Q_UNUSED(sourceRight) Q_UNUSED(proxyModel) return false; } void Sorter::invalidate() { if (m_enabled) Q_EMIT invalidated(); } } spectral/include/SortFilterProxyModel/sorters/expressionsorter.cpp0000644000175000000620000001176013566674121025776 0ustar dilingerstaff#include "expressionsorter.h" #include "qqmlsortfilterproxymodel.h" #include namespace qqsfpm { /*! \qmltype ExpressionSorter \inherits Sorter \inqmlmodule SortFilterProxyModel \brief Sorts row with a custom sorting An ExpressionSorter is a \l Sorter allowing to implement custom sorting based on a javascript expression. */ /*! \qmlproperty expression ExpressionSorter::expression An expression to implement custom sorting. It must evaluate to a bool. It has the same syntax has a \l {http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html} {Property Binding}, except that it will be evaluated for each of the source model's rows. Model data is accessible for both rows with the \c modelLeft, and \c modelRight properties: \code sorters: ExpressionSorter { expression: { return modelLeft.someRole < modelRight.someRole; } } \endcode The \c index of the row is also available through \c modelLeft and \c modelRight. The expression should return \c true if the value of the left item is less than the value of the right item, otherwise returns false. This expression is reevaluated for a row every time its model data changes. When an external property (not \c index* or in \c model*) the expression depends on changes, the expression is reevaluated for every row of the source model. To capture the properties the expression depends on, the expression is first executed with invalid data and each property access is detected by the QML engine. This means that if a property is not accessed because of a conditional, it won't be captured and the expression won't be reevaluted when this property changes. A workaround to this problem is to access all the properties the expressions depends unconditionally at the beggining of the expression. */ const QQmlScriptString& ExpressionSorter::expression() const { return m_scriptString; } void ExpressionSorter::setExpression(const QQmlScriptString& scriptString) { if (m_scriptString == scriptString) return; m_scriptString = scriptString; updateExpression(); Q_EMIT expressionChanged(); invalidate(); } void ExpressionSorter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) { updateContext(proxyModel); } bool evaluateBoolExpression(QQmlExpression& expression) { QVariant variantResult = expression.evaluate(); if (expression.hasError()) { qWarning() << expression.error(); return false; } if (variantResult.canConvert()) { return variantResult.toBool(); } else { qWarning("%s:%i:%i : Can't convert result to bool", expression.sourceFile().toUtf8().data(), expression.lineNumber(), expression.columnNumber()); return false; } } int ExpressionSorter::compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const { if (!m_scriptString.isEmpty()) { QVariantMap modelLeftMap, modelRightMap; QHash roles = proxyModel.roleNames(); QQmlContext context(qmlContext(this)); for (auto it = roles.cbegin(); it != roles.cend(); ++it) { modelLeftMap.insert(it.value(), proxyModel.sourceData(sourceLeft, it.key())); modelRightMap.insert(it.value(), proxyModel.sourceData(sourceRight, it.key())); } modelLeftMap.insert("index", sourceLeft.row()); modelRightMap.insert("index", sourceRight.row()); QQmlExpression expression(m_scriptString, &context); context.setContextProperty("modelLeft", modelLeftMap); context.setContextProperty("modelRight", modelRightMap); if (evaluateBoolExpression(expression)) return -1; context.setContextProperty("modelLeft", modelRightMap); context.setContextProperty("modelRight", modelLeftMap); if (evaluateBoolExpression(expression)) return 1; } return 0; } void ExpressionSorter::updateContext(const QQmlSortFilterProxyModel& proxyModel) { delete m_context; m_context = new QQmlContext(qmlContext(this), this); QVariantMap modelLeftMap, modelRightMap; // what about roles changes ? for (const QByteArray& roleName : proxyModel.roleNames().values()) { modelLeftMap.insert(roleName, QVariant()); modelRightMap.insert(roleName, QVariant()); } modelLeftMap.insert("index", -1); modelRightMap.insert("index", -1); m_context->setContextProperty("modelLeft", modelLeftMap); m_context->setContextProperty("modelRight", modelRightMap); updateExpression(); } void ExpressionSorter::updateExpression() { if (!m_context) return; delete m_expression; m_expression = new QQmlExpression(m_scriptString, m_context, 0, this); connect(m_expression, &QQmlExpression::valueChanged, this, &ExpressionSorter::invalidate); m_expression->setNotifyOnValueChanged(true); m_expression->evaluate(); } } spectral/include/SortFilterProxyModel/sorters/filtersorter.h0000644000175000000620000000164213566674121024527 0ustar dilingerstaff#ifndef FILTERSORTER_H #define FILTERSORTER_H #include "sorter.h" #include "filters/filtercontainer.h" namespace qqsfpm { class FilterSorter : public Sorter, public FilterContainer { Q_OBJECT Q_INTERFACES(qqsfpm::FilterContainer) Q_PROPERTY(QQmlListProperty filters READ filtersListProperty) Q_CLASSINFO("DefaultProperty", "filters") public: using Sorter::Sorter; protected: int compare(const QModelIndex &sourceLeft, const QModelIndex &sourceRight, const QQmlSortFilterProxyModel &proxyModel) const override; private: void proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) override; void onFilterAppended(Filter *filter) override; void onFilterRemoved(Filter *filter) override; void onFiltersCleared() override; bool indexIsAccepted(const QModelIndex &sourceIndex, const QQmlSortFilterProxyModel &proxyModel) const; }; } #endif // FILTERSORTER_H spectral/include/SortFilterProxyModel/sorters/sorter.h0000644000175000000620000000271313566674121023321 0ustar dilingerstaff#ifndef SORTER_H #define SORTER_H #include namespace qqsfpm { class QQmlSortFilterProxyModel; class Sorter : public QObject { Q_OBJECT Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) Q_PROPERTY(bool ascendingOrder READ ascendingOrder WRITE setAscendingOrder NOTIFY sortOrderChanged) Q_PROPERTY(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged) public: Sorter(QObject* parent = nullptr); virtual ~Sorter() = 0; bool enabled() const; void setEnabled(bool enabled); bool ascendingOrder() const; void setAscendingOrder(bool ascendingOrder); Qt::SortOrder sortOrder() const; void setSortOrder(Qt::SortOrder sortOrder); int compareRows(const QModelIndex& source_left, const QModelIndex& source_right, const QQmlSortFilterProxyModel& proxyModel) const; virtual void proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel); Q_SIGNALS: void enabledChanged(); void sortOrderChanged(); void invalidated(); protected: virtual int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const; virtual bool lessThan(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const; void invalidate(); private: bool m_enabled = true; Qt::SortOrder m_sortOrder = Qt::AscendingOrder; }; } #endif // SORTER_H spectral/include/SortFilterProxyModel/sorters/expressionsorter.h0000644000175000000620000000200113566674121025427 0ustar dilingerstaff#ifndef EXPRESSIONSORTER_H #define EXPRESSIONSORTER_H #include "sorter.h" #include class QQmlExpression; namespace qqsfpm { class QQmlSortFilterProxyModel; class ExpressionSorter : public Sorter { Q_OBJECT Q_PROPERTY(QQmlScriptString expression READ expression WRITE setExpression NOTIFY expressionChanged) public: using Sorter::Sorter; const QQmlScriptString& expression() const; void setExpression(const QQmlScriptString& scriptString); void proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) override; Q_SIGNALS: void expressionChanged(); protected: int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const override; private: void updateContext(const QQmlSortFilterProxyModel& proxyModel); void updateExpression(); QQmlScriptString m_scriptString; QQmlExpression* m_expression = nullptr; QQmlContext* m_context = nullptr; }; } #endif // EXPRESSIONSORTER_H spectral/include/SortFilterProxyModel/sorters/filtersorter.cpp0000644000175000000620000000410213566674121025054 0ustar dilingerstaff#include "filtersorter.h" #include "filters/filter.h" namespace qqsfpm { /*! \qmltype FilterSorter \inherits Sorter \inqmlmodule SortFilterProxyModel \brief Sorts rows based on if they match filters A FilterSorter is a \l Sorter that orders row matching its filters before the rows not matching the filters. In the following example, rows with their \c favorite role set to \c true will be ordered at the beginning : \code SortFilterProxyModel { sourceModel: contactModel sorters: FilterSorter { ValueFilter { roleName: "favorite"; value: true } } } \endcode */ /*! \qmlproperty string FilterSorter::filters This property holds the list of filters for this filter sorter. If a row match all this FilterSorter's filters, it will be ordered before rows not matching all the filters. \sa Filter */ int FilterSorter::compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel &proxyModel) const { bool leftIsAccepted = indexIsAccepted(sourceLeft, proxyModel); bool rightIsAccepted = indexIsAccepted(sourceRight, proxyModel); if (leftIsAccepted == rightIsAccepted) return 0; return leftIsAccepted ? -1 : 1; } void FilterSorter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) { for (Filter* filter : m_filters) filter->proxyModelCompleted(proxyModel); } void FilterSorter::onFilterAppended(Filter* filter) { connect(filter, &Filter::invalidated, this, &FilterSorter::invalidate); invalidate(); } void FilterSorter::onFilterRemoved(Filter* filter) { disconnect(filter, &Filter::invalidated, this, &FilterSorter::invalidate); invalidate(); } void FilterSorter::onFiltersCleared() { invalidate(); } bool FilterSorter::indexIsAccepted(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const { return std::all_of(m_filters.begin(), m_filters.end(), [&] (Filter* filter) { return filter->filterAcceptsRow(sourceIndex, proxyModel); } ); } } spectral/include/SortFilterProxyModel/sorters/sortersqmltypes.cpp0000644000175000000620000000131713566674121025635 0ustar dilingerstaff#include "sorter.h" #include "rolesorter.h" #include "stringsorter.h" #include "filtersorter.h" #include "expressionsorter.h" #include #include namespace qqsfpm { void registerSorterTypes() { qmlRegisterUncreatableType("SortFilterProxyModel", 0, 2, "Sorter", "Sorter is an abstract class"); qmlRegisterType("SortFilterProxyModel", 0, 2, "RoleSorter"); qmlRegisterType("SortFilterProxyModel", 0, 2, "StringSorter"); qmlRegisterType("SortFilterProxyModel", 0, 2, "FilterSorter"); qmlRegisterType("SortFilterProxyModel", 0, 2, "ExpressionSorter"); } Q_COREAPP_STARTUP_FUNCTION(registerSorterTypes) } spectral/include/SortFilterProxyModel/sorters/stringsorter.h0000644000175000000620000000247613566674121024556 0ustar dilingerstaff#ifndef STRINGSORTER_H #define STRINGSORTER_H #include "rolesorter.h" #include namespace qqsfpm { class StringSorter : public RoleSorter { Q_OBJECT Q_PROPERTY(Qt::CaseSensitivity caseSensitivity READ caseSensitivity WRITE setCaseSensitivity NOTIFY caseSensitivityChanged) Q_PROPERTY(bool ignorePunctation READ ignorePunctation WRITE setIgnorePunctation NOTIFY ignorePunctationChanged) Q_PROPERTY(QLocale locale READ locale WRITE setLocale NOTIFY localeChanged) Q_PROPERTY(bool numericMode READ numericMode WRITE setNumericMode NOTIFY numericModeChanged) public: using RoleSorter::RoleSorter; Qt::CaseSensitivity caseSensitivity() const; void setCaseSensitivity(Qt::CaseSensitivity caseSensitivity); bool ignorePunctation() const; void setIgnorePunctation(bool ignorePunctation); QLocale locale() const; void setLocale(const QLocale& locale); bool numericMode() const; void setNumericMode(bool numericMode); Q_SIGNALS: void caseSensitivityChanged(); void ignorePunctationChanged(); void localeChanged(); void numericModeChanged(); protected: int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const override; private: QCollator m_collator; }; } #endif // STRINGSORTER_H spectral/include/SortFilterProxyModel/sorters/rolesorter.h0000644000175000000620000000135613566674121024205 0ustar dilingerstaff#ifndef ROLESORTER_H #define ROLESORTER_H #include "sorter.h" namespace qqsfpm { class RoleSorter : public Sorter { Q_OBJECT Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged) public: using Sorter::Sorter; const QString& roleName() const; void setRoleName(const QString& roleName); Q_SIGNALS: void roleNameChanged(); protected: QPair sourceData(const QModelIndex &sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const; int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const QQmlSortFilterProxyModel& proxyModel) const override; private: QString m_roleName; }; } #endif // ROLESORTER_H spectral/include/SortFilterProxyModel/proxyroles/0002755000175000000620000000000013566674121022356 5ustar dilingerstaffspectral/include/SortFilterProxyModel/proxyroles/proxyrole.h0000644000175000000620000000145413566674121024574 0ustar dilingerstaff#ifndef PROXYROLE_H #define PROXYROLE_H #include #include namespace qqsfpm { class QQmlSortFilterProxyModel; class ProxyRole : public QObject { Q_OBJECT public: using QObject::QObject; virtual ~ProxyRole() = default; QVariant roleData(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel, const QString& name); virtual void proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel); virtual QStringList names() = 0; protected: void invalidate(); Q_SIGNALS: void invalidated(); void namesAboutToBeChanged(); void namesChanged(); private: virtual QVariant data(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel, const QString& name) = 0; QMutex m_mutex; }; } #endif // PROXYROLE_H spectral/include/SortFilterProxyModel/proxyroles/expressionrole.cpp0000644000175000000620000001025313566674121026142 0ustar dilingerstaff#include "expressionrole.h" #include "qqmlsortfilterproxymodel.h" #include namespace qqsfpm { /*! \qmltype ExpressionRole \inherits SingleRole \inqmlmodule SortFilterProxyModel \brief A custom role computed from a javascrip expression An ExpressionRole is a \l ProxyRole allowing to implement a custom role based on a javascript expression. In the following example, the \c c role is computed by adding the \c a role and \c b role of the model : \code SortFilterProxyModel { sourceModel: numberModel proxyRoles: ExpressionRole { name: "c" expression: model.a + model.b } } \endcode */ /*! \qmlproperty expression ExpressionRole::expression An expression to implement a custom role. It has the same syntax has a \l {http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html} {Property Binding} except it will be evaluated for each of the source model's rows. The data for this role will be the retuned valued of the expression. Data for each row is exposed like for a delegate of a QML View. This expression is reevaluated for a row every time its model data changes. When an external property (not \c index or in \c model) the expression depends on changes, the expression is reevaluated for every row of the source model. To capture the properties the expression depends on, the expression is first executed with invalid data and each property access is detected by the QML engine. This means that if a property is not accessed because of a conditional, it won't be captured and the expression won't be reevaluted when this property changes. A workaround to this problem is to access all the properties the expressions depends unconditionally at the beggining of the expression. */ const QQmlScriptString& ExpressionRole::expression() const { return m_scriptString; } void ExpressionRole::setExpression(const QQmlScriptString& scriptString) { if (m_scriptString == scriptString) return; m_scriptString = scriptString; updateExpression(); Q_EMIT expressionChanged(); invalidate(); } void ExpressionRole::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) { updateContext(proxyModel); } QVariant ExpressionRole::data(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) { if (!m_scriptString.isEmpty()) { QVariantMap modelMap; QHash roles = proxyModel.roleNames(); QQmlContext context(qmlContext(this)); auto addToContext = [&] (const QString &name, const QVariant& value) { context.setContextProperty(name, value); modelMap.insert(name, value); }; for (auto it = roles.cbegin(); it != roles.cend(); ++it) addToContext(it.value(), proxyModel.sourceData(sourceIndex, it.key())); addToContext("index", sourceIndex.row()); context.setContextProperty("model", modelMap); QQmlExpression expression(m_scriptString, &context); QVariant result = expression.evaluate(); if (expression.hasError()) { qWarning() << expression.error(); return true; } return result; } return QVariant(); } void ExpressionRole::updateContext(const QQmlSortFilterProxyModel& proxyModel) { delete m_context; m_context = new QQmlContext(qmlContext(this), this); // what about roles changes ? QVariantMap modelMap; auto addToContext = [&] (const QString &name, const QVariant& value) { m_context->setContextProperty(name, value); modelMap.insert(name, value); }; for (const QByteArray& roleName : proxyModel.roleNames().values()) addToContext(roleName, QVariant()); addToContext("index", -1); m_context->setContextProperty("model", modelMap); updateExpression(); } void ExpressionRole::updateExpression() { if (!m_context) return; delete m_expression; m_expression = new QQmlExpression(m_scriptString, m_context, 0, this); connect(m_expression, &QQmlExpression::valueChanged, this, &ExpressionRole::invalidate); m_expression->setNotifyOnValueChanged(true); m_expression->evaluate(); } } spectral/include/SortFilterProxyModel/proxyroles/proxyrole.cpp0000644000175000000620000000216413566674121025126 0ustar dilingerstaff#include "proxyrole.h" #include #include #include #include #include #include #include "filters/filter.h" #include "qqmlsortfilterproxymodel.h" namespace qqsfpm { /*! \qmltype ProxyRole \inqmlmodule SortFilterProxyModel \brief Base type for the \l SortFilterProxyModel proxy roles The ProxyRole type cannot be used directly in a QML file. It exists to provide a set of common properties and methods, available across all the other proxy role types that inherit from it. Attempting to use the ProxyRole type directly will result in an error. */ QVariant ProxyRole::roleData(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel, const QString &name) { if (m_mutex.tryLock()) { QVariant result = data(sourceIndex, proxyModel, name); m_mutex.unlock(); return result; } else { return {}; } } void ProxyRole::proxyModelCompleted(const QQmlSortFilterProxyModel &proxyModel) { Q_UNUSED(proxyModel) } void ProxyRole::invalidate() { Q_EMIT invalidated(); } } spectral/include/SortFilterProxyModel/proxyroles/singlerole.cpp0000644000175000000620000000230413566674121025222 0ustar dilingerstaff#include "singlerole.h" #include namespace qqsfpm { /*! \qmltype SingleRole \inherits ProxyRole \inqmlmodule SortFilterProxyModel \brief Base type for the \l SortFilterProxyModel proxy roles defining a single role SingleRole is a convenience base class for proxy roles who define a single role. It cannot be used directly in a QML file. It exists to provide a set of common properties and methods, available across all the other proxy role types that inherit from it. Attempting to use the SingleRole type directly will result in an error. */ /*! \qmlproperty string SingleRole::name This property holds the role name of the proxy role. */ QString SingleRole::name() const { return m_name; } void SingleRole::setName(const QString& name) { if (m_name == name) return; Q_EMIT namesAboutToBeChanged(); m_name = name; Q_EMIT nameChanged(); Q_EMIT namesChanged(); } QStringList SingleRole::names() { return QStringList { m_name }; } QVariant SingleRole::data(const QModelIndex &sourceIndex, const QQmlSortFilterProxyModel &proxyModel, const QString &name) { Q_UNUSED(name); return data(sourceIndex, proxyModel); } } spectral/include/SortFilterProxyModel/proxyroles/filterrole.h0000644000175000000620000000127413566674121024700 0ustar dilingerstaff#ifndef FILTERROLE_H #define FILTERROLE_H #include "singlerole.h" #include "filters/filtercontainer.h" namespace qqsfpm { class FilterRole : public SingleRole, public FilterContainer { Q_OBJECT Q_INTERFACES(qqsfpm::FilterContainer) Q_PROPERTY(QQmlListProperty filters READ filtersListProperty) Q_CLASSINFO("DefaultProperty", "filters") public: using SingleRole::SingleRole; private: void onFilterAppended(Filter* filter) override; void onFilterRemoved(Filter* filter) override; void onFiltersCleared() override; QVariant data(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) override; }; } #endif // FILTERROLE_H spectral/include/SortFilterProxyModel/proxyroles/regexprole.h0000644000175000000620000000220013566674121024673 0ustar dilingerstaff#ifndef REGEXPROLE_H #define REGEXPROLE_H #include "proxyrole.h" #include namespace qqsfpm { class RegExpRole : public ProxyRole { Q_OBJECT Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged) Q_PROPERTY(QString pattern READ pattern WRITE setPattern NOTIFY patternChanged) Q_PROPERTY(Qt::CaseSensitivity caseSensitivity READ caseSensitivity WRITE setCaseSensitivity NOTIFY caseSensitivityChanged) public: using ProxyRole::ProxyRole; QString roleName() const; void setRoleName(const QString& roleName); QString pattern() const; void setPattern(const QString& pattern); Qt::CaseSensitivity caseSensitivity() const; void setCaseSensitivity(Qt::CaseSensitivity caseSensitivity); QStringList names() override; Q_SIGNALS: void roleNameChanged(); void patternChanged(); void caseSensitivityChanged(); private: QString m_roleName; QRegularExpression m_regularExpression; QVariant data(const QModelIndex &sourceIndex, const QQmlSortFilterProxyModel &proxyModel, const QString &name) override; }; } #endif // REGEXPROLE_H spectral/include/SortFilterProxyModel/proxyroles/expressionrole.h0000644000175000000620000000167113566674121025613 0ustar dilingerstaff#ifndef EXPRESSIONROLE_H #define EXPRESSIONROLE_H #include "singlerole.h" #include class QQmlExpression; namespace qqsfpm { class ExpressionRole : public SingleRole { Q_OBJECT Q_PROPERTY(QQmlScriptString expression READ expression WRITE setExpression NOTIFY expressionChanged) public: using SingleRole::SingleRole; const QQmlScriptString& expression() const; void setExpression(const QQmlScriptString& scriptString); void proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) override; Q_SIGNALS: void expressionChanged(); private: QVariant data(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) override; void updateContext(const QQmlSortFilterProxyModel& proxyModel); void updateExpression(); QQmlScriptString m_scriptString; QQmlExpression* m_expression = nullptr; QQmlContext* m_context = nullptr; }; } #endif // EXPRESSIONROLE_H spectral/include/SortFilterProxyModel/proxyroles/filterrole.cpp0000644000175000000620000000307113566674121025230 0ustar dilingerstaff#include "filterrole.h" #include "filters/filter.h" namespace qqsfpm { /*! \qmltype FilterRole \inherits SingleRole \inqmlmodule SortFilterProxyModel \brief A role resolving to \c true for rows matching all its filters A FilterRole is a \l ProxyRole that returns \c true for rows matching all its filters. In the following example, the \c isAdult role will be equal to \c true if the \c age role is superior or equal to 18. \code SortFilterProxyModel { sourceModel: personModel proxyRoles: FilterRole { name: "isAdult" RangeFilter { roleName: "age"; minimumValue: 18; minimumInclusive: true } } } \endcode */ /*! \qmlproperty string FilterRole::filters This property holds the list of filters for this filter role. The data of this role will be equal to the \c true if all its filters match the model row, \c false otherwise. \sa Filter */ void FilterRole::onFilterAppended(Filter* filter) { connect(filter, &Filter::invalidated, this, &FilterRole::invalidate); invalidate(); } void FilterRole::onFilterRemoved(Filter* filter) { disconnect(filter, &Filter::invalidated, this, &FilterRole::invalidate); invalidate(); } void FilterRole::onFiltersCleared() { invalidate(); } QVariant FilterRole::data(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) { return std::all_of(m_filters.begin(), m_filters.end(), [&] (Filter* filter) { return filter->filterAcceptsRow(sourceIndex, proxyModel); } ); } } spectral/include/SortFilterProxyModel/proxyroles/regexprole.cpp0000644000175000000620000000565713566674121025251 0ustar dilingerstaff#include "regexprole.h" #include "qqmlsortfilterproxymodel.h" #include namespace qqsfpm { /*! \qmltype RegExpRole \inherits ProxyRole \inqmlmodule SortFilterProxyModel \brief A ProxyRole extracting data from a source role via a regular expression. A RegExpRole is a \l ProxyRole that provides a role for each named capture group of its regular expression \l pattern. In the following example, the \c date role of the source model will be extracted in 3 roles in the proxy moodel: \c year, \c month and \c day. \code SortFilterProxyModel { sourceModel: eventModel proxyRoles: RegExpRole { roleName: "date" pattern: "(?\\d{4})-(?\\d{2})-(?\\d{2})" } } \endcode */ /*! \qmlproperty QString RegExpRole::roleName This property holds the role name that the RegExpRole is using to query the source model's data to extract new roles from. */ QString RegExpRole::roleName() const { return m_roleName; } void RegExpRole::setRoleName(const QString& roleName) { if (m_roleName == roleName) return; m_roleName = roleName; Q_EMIT roleNameChanged(); } /*! \qmlproperty QString RegExpRole::pattern This property holds the pattern of the regular expression of this RegExpRole. The RegExpRole will expose a role for each of the named capture group of the pattern. */ QString RegExpRole::pattern() const { return m_regularExpression.pattern(); } void RegExpRole::setPattern(const QString& pattern) { if (m_regularExpression.pattern() == pattern) return; Q_EMIT namesAboutToBeChanged(); m_regularExpression.setPattern(pattern); invalidate(); Q_EMIT patternChanged(); Q_EMIT namesChanged(); } /*! \qmlproperty Qt::CaseSensitivity RegExpRole::caseSensitivity This property holds the caseSensitivity of the regular expression. */ Qt::CaseSensitivity RegExpRole::caseSensitivity() const { return m_regularExpression.patternOptions() & QRegularExpression::CaseInsensitiveOption ? Qt::CaseInsensitive : Qt::CaseSensitive; } void RegExpRole::setCaseSensitivity(Qt::CaseSensitivity caseSensitivity) { if (this->caseSensitivity() == caseSensitivity) return; m_regularExpression.setPatternOptions(m_regularExpression.patternOptions() ^ QRegularExpression::CaseInsensitiveOption); //toggle the option Q_EMIT caseSensitivityChanged(); } QStringList RegExpRole::names() { QStringList nameCaptureGroups = m_regularExpression.namedCaptureGroups(); nameCaptureGroups.removeAll(""); return nameCaptureGroups; } QVariant RegExpRole::data(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel, const QString &name) { QString text = proxyModel.sourceData(sourceIndex, m_roleName).toString(); QRegularExpressionMatch match = m_regularExpression.match(text); return match.hasMatch() ? (match.captured(name)) : QVariant{}; } } spectral/include/SortFilterProxyModel/proxyroles/proxyrolesqmltypes.cpp0000644000175000000620000000145613566674121027113 0ustar dilingerstaff#include "proxyrole.h" #include "joinrole.h" #include "switchrole.h" #include "expressionrole.h" #include "regexprole.h" #include "filterrole.h" #include #include namespace qqsfpm { void registerProxyRoleTypes() { qmlRegisterUncreatableType("SortFilterProxyModel", 0, 2, "ProxyRole", "ProxyRole is an abstract class"); qmlRegisterType("SortFilterProxyModel", 0, 2, "JoinRole"); qmlRegisterType("SortFilterProxyModel", 0, 2, "SwitchRole"); qmlRegisterType("SortFilterProxyModel", 0, 2, "ExpressionRole"); qmlRegisterType("SortFilterProxyModel", 0, 2, "RegExpRole"); qmlRegisterType("SortFilterProxyModel", 0, 2, "FilterRole"); } Q_COREAPP_STARTUP_FUNCTION(registerProxyRoleTypes) } spectral/include/SortFilterProxyModel/proxyroles/switchrole.h0000644000175000000620000000343013566674121024710 0ustar dilingerstaff#ifndef SWITCHROLE_H #define SWITCHROLE_H #include "singlerole.h" #include "filters/filtercontainer.h" #include namespace qqsfpm { class SwitchRoleAttached : public QObject { Q_OBJECT Q_PROPERTY(QVariant value READ value WRITE setValue NOTIFY valueChanged) public: SwitchRoleAttached(QObject* parent); QVariant value() const; void setValue(QVariant value); Q_SIGNALS: void valueChanged(); private: QVariant m_value; }; class SwitchRole : public SingleRole, public FilterContainer { Q_OBJECT Q_INTERFACES(qqsfpm::FilterContainer) Q_PROPERTY(QString defaultRoleName READ defaultRoleName WRITE setDefaultRoleName NOTIFY defaultRoleNameChanged) Q_PROPERTY(QVariant defaultValue READ defaultValue WRITE setDefaultValue NOTIFY defaultValueChanged) Q_PROPERTY(QQmlListProperty filters READ filtersListProperty) Q_CLASSINFO("DefaultProperty", "filters") public: using SingleRole::SingleRole; QString defaultRoleName() const; void setDefaultRoleName(const QString& defaultRoleName); QVariant defaultValue() const; void setDefaultValue(const QVariant& defaultValue); void proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) override; static SwitchRoleAttached* qmlAttachedProperties(QObject* object); Q_SIGNALS: void defaultRoleNameChanged(); void defaultValueChanged(); private: QVariant data(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) override; void onFilterAppended(Filter *filter) override; void onFilterRemoved(Filter *filter) override; void onFiltersCleared() override; QString m_defaultRoleName; QVariant m_defaultValue; }; } QML_DECLARE_TYPEINFO(qqsfpm::SwitchRole, QML_HAS_ATTACHED_PROPERTIES) #endif // SWITCHROLE_H spectral/include/SortFilterProxyModel/proxyroles/singlerole.h0000644000175000000620000000125713566674121024675 0ustar dilingerstaff#ifndef SINGLEROLE_H #define SINGLEROLE_H #include "proxyrole.h" namespace qqsfpm { class SingleRole : public ProxyRole { Q_OBJECT Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) public: using ProxyRole::ProxyRole; QString name() const; void setName(const QString& name); QStringList names() override; Q_SIGNALS: void nameChanged(); private: QString m_name; private: QVariant data(const QModelIndex &sourceIndex, const QQmlSortFilterProxyModel &proxyModel, const QString &name) final; virtual QVariant data(const QModelIndex &sourceIndex, const QQmlSortFilterProxyModel &proxyModel) = 0; }; } #endif // SINGLEROLE_H spectral/include/SortFilterProxyModel/proxyroles/joinrole.cpp0000644000175000000620000000346313566674121024707 0ustar dilingerstaff#include "joinrole.h" #include "qqmlsortfilterproxymodel.h" namespace qqsfpm { /*! \qmltype JoinRole \inherits SingleRole \inqmlmodule SortFilterProxyModel \brief a role made from concatenating other roles A JoinRole is a simple \l ProxyRole that concatenates other roles. In the following example, the \c fullName role is computed by the concatenation of the \c firstName role and the \c lastName role separated by a space : \code SortFilterProxyModel { sourceModel: contactModel proxyRoles: JoinRole { name: "fullName" roleNames: ["firstName", "lastName"] } } \endcode */ /*! \qmlproperty list JoinRole::roleNames This property holds the role names that are joined by this role. */ QStringList JoinRole::roleNames() const { return m_roleNames; } void JoinRole::setRoleNames(const QStringList& roleNames) { if (m_roleNames == roleNames) return; m_roleNames = roleNames; Q_EMIT roleNamesChanged(); invalidate(); } /*! \qmlproperty string JoinRole::separator This property holds the separator that is used to join the roles specified in \l roleNames. By default, it's a space. */ QString JoinRole::separator() const { return m_separator; } void JoinRole::setSeparator(const QString& separator) { if (m_separator == separator) return; m_separator = separator; Q_EMIT separatorChanged(); invalidate(); } QVariant JoinRole::data(const QModelIndex &sourceIndex, const QQmlSortFilterProxyModel& proxyModel) { QString result; for (const QString& roleName : m_roleNames) result += proxyModel.sourceData(sourceIndex, roleName).toString() + m_separator; if (!m_roleNames.isEmpty()) result.chop(m_separator.length()); return result; } } spectral/include/SortFilterProxyModel/proxyroles/proxyrolecontainer.cpp0000644000175000000620000000355013566674121027031 0ustar dilingerstaff#include "proxyrolecontainer.h" namespace qqsfpm { ProxyRoleContainer::~ProxyRoleContainer() { } QList ProxyRoleContainer::proxyRoles() const { return m_proxyRoles; } void ProxyRoleContainer::appendProxyRole(ProxyRole* proxyRole) { m_proxyRoles.append(proxyRole); onProxyRoleAppended(proxyRole); } void ProxyRoleContainer::removeProxyRole(ProxyRole* proxyRole) { m_proxyRoles.removeOne(proxyRole); onProxyRoleRemoved(proxyRole); } void ProxyRoleContainer::clearProxyRoles() { m_proxyRoles.clear(); onProxyRolesCleared(); } QQmlListProperty ProxyRoleContainer::proxyRolesListProperty() { return QQmlListProperty(reinterpret_cast(this), &m_proxyRoles, &ProxyRoleContainer::append_proxyRole, &ProxyRoleContainer::count_proxyRole, &ProxyRoleContainer::at_proxyRole, &ProxyRoleContainer::clear_proxyRoles); } void ProxyRoleContainer::append_proxyRole(QQmlListProperty* list, ProxyRole* proxyRole) { if (!proxyRole) return; ProxyRoleContainer* that = reinterpret_cast(list->object); that->appendProxyRole(proxyRole); } int ProxyRoleContainer::count_proxyRole(QQmlListProperty* list) { QList* ProxyRoles = static_cast*>(list->data); return ProxyRoles->count(); } ProxyRole* ProxyRoleContainer::at_proxyRole(QQmlListProperty* list, int index) { QList* ProxyRoles = static_cast*>(list->data); return ProxyRoles->at(index); } void ProxyRoleContainer::clear_proxyRoles(QQmlListProperty *list) { ProxyRoleContainer* that = reinterpret_cast(list->object); that->clearProxyRoles(); } } spectral/include/SortFilterProxyModel/proxyroles/joinrole.h0000644000175000000620000000145013566674121024346 0ustar dilingerstaff#ifndef JOINROLE_H #define JOINROLE_H #include "singlerole.h" namespace qqsfpm { class JoinRole : public SingleRole { Q_OBJECT Q_PROPERTY(QStringList roleNames READ roleNames WRITE setRoleNames NOTIFY roleNamesChanged) Q_PROPERTY(QString separator READ separator WRITE setSeparator NOTIFY separatorChanged) public: using SingleRole::SingleRole; QStringList roleNames() const; void setRoleNames(const QStringList& roleNames); QString separator() const; void setSeparator(const QString& separator); Q_SIGNALS: void roleNamesChanged(); void separatorChanged(); private: QStringList m_roleNames; QVariant data(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) override; QString m_separator = " "; }; } #endif // JOINROLE_H spectral/include/SortFilterProxyModel/proxyroles/proxyrolecontainer.h0000644000175000000620000000226413566674121026477 0ustar dilingerstaff#ifndef PROXYROLECONTAINER_H #define PROXYROLECONTAINER_H #include #include namespace qqsfpm { class ProxyRole; class QQmlSortFilterProxyModel; class ProxyRoleContainer { public: virtual ~ProxyRoleContainer(); QList proxyRoles() const; void appendProxyRole(ProxyRole* proxyRole); void removeProxyRole(ProxyRole* proxyRole); void clearProxyRoles(); QQmlListProperty proxyRolesListProperty(); protected: QList m_proxyRoles; private: virtual void onProxyRoleAppended(ProxyRole* proxyRole) = 0; virtual void onProxyRoleRemoved(ProxyRole* proxyRole) = 0; virtual void onProxyRolesCleared() = 0; static void append_proxyRole(QQmlListProperty* list, ProxyRole* proxyRole); static int count_proxyRole(QQmlListProperty* list); static ProxyRole* at_proxyRole(QQmlListProperty* list, int index); static void clear_proxyRoles(QQmlListProperty* list); }; } #define ProxyRoleContainer_iid "fr.grecko.SortFilterProxyModel.ProxyRoleContainer" Q_DECLARE_INTERFACE(qqsfpm::ProxyRoleContainer, ProxyRoleContainer_iid) #endif // PROXYROLECONTAINER_H spectral/include/SortFilterProxyModel/proxyroles/switchrole.cpp0000644000175000000620000001144013566674121025243 0ustar dilingerstaff#include "switchrole.h" #include "qqmlsortfilterproxymodel.h" #include "filters/filter.h" #include namespace qqsfpm { /*! \qmltype SwitchRole \inherits SingleRole \inqmlmodule SortFilterProxyModel \brief A role using \l Filter to conditionnaly compute its data A SwitchRole is a \l ProxyRole that computes its data with the help of \l Filter. Each top level filters specified in the \l SwitchRole is evaluated on the rows of the model, if a \l Filter evaluates to true, the data of the \l SwitchRole for this row will be the one of the attached \l {value} {SwitchRole.value} property. If no top level filters evaluate to true, the data will default to the one of the \l defaultRoleName (or the \l defaultValue if no \l defaultRoleName is specified). In the following example, the \c favoriteOrFirstNameSection role is equal to \c * if the \c favorite role of a row is true, otherwise it's the same as the \c firstName role : \code SortFilterProxyModel { sourceModel: contactModel proxyRoles: SwitchRole { name: "favoriteOrFirstNameSection" filters: ValueFilter { roleName: "favorite" value: true SwitchRole.value: "*" } defaultRoleName: "firstName" } } \endcode */ SwitchRoleAttached::SwitchRoleAttached(QObject* parent) : QObject (parent) { if (!qobject_cast(parent)) qmlInfo(parent) << "SwitchRole must be attached to a Filter"; } /*! \qmlattachedproperty var SwitchRole::value This property attaches a value to a \l Filter. */ QVariant SwitchRoleAttached::value() const { return m_value; } void SwitchRoleAttached::setValue(QVariant value) { if (m_value == value) return; m_value = value; Q_EMIT valueChanged(); } /*! \qmlproperty string SwitchRole::defaultRoleName This property holds the default role name of the role. If no filter match a row, the data of this role will be the data of the role whose name is \c defaultRoleName. */ QString SwitchRole::defaultRoleName() const { return m_defaultRoleName; } void SwitchRole::setDefaultRoleName(const QString& defaultRoleName) { if (m_defaultRoleName == defaultRoleName) return; m_defaultRoleName = defaultRoleName; Q_EMIT defaultRoleNameChanged(); invalidate(); } /*! \qmlproperty var SwitchRole::defaultValue This property holds the default value of the role. If no filter match a row, and no \l defaultRoleName is set, the data of this role will be \c defaultValue. */ QVariant SwitchRole::defaultValue() const { return m_defaultValue; } void SwitchRole::setDefaultValue(const QVariant& defaultValue) { if (m_defaultValue == defaultValue) return; m_defaultValue = defaultValue; Q_EMIT defaultValueChanged(); invalidate(); } /*! \qmlproperty list SwitchRole::filters This property holds the list of filters for this proxy role. The data of this role will be equal to the attached \l {value} {SwitchRole.value} property of the first filter that matches the model row. \sa Filter */ void SwitchRole::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) { for (Filter* filter : m_filters) filter->proxyModelCompleted(proxyModel); } SwitchRoleAttached* SwitchRole::qmlAttachedProperties(QObject* object) { return new SwitchRoleAttached(object); } QVariant SwitchRole::data(const QModelIndex &sourceIndex, const QQmlSortFilterProxyModel &proxyModel) { for (auto filter: m_filters) { if (!filter->enabled()) continue; if (filter->filterAcceptsRow(sourceIndex, proxyModel)) { auto attached = static_cast(qmlAttachedPropertiesObject(filter, false)); if (!attached) { qWarning() << "No SwitchRole.value provided for this filter" << filter; continue; } QVariant value = attached->value(); if (!value.isValid()) { qWarning() << "No SwitchRole.value provided for this filter" << filter; continue; } return value; } } if (!m_defaultRoleName.isEmpty()) return proxyModel.sourceData(sourceIndex, m_defaultRoleName); return m_defaultValue; } void SwitchRole::onFilterAppended(Filter *filter) { connect(filter, &Filter::invalidated, this, &SwitchRole::invalidate); auto attached = static_cast(qmlAttachedPropertiesObject(filter, true)); connect(attached, &SwitchRoleAttached::valueChanged, this, &SwitchRole::invalidate); invalidate(); } void SwitchRole::onFilterRemoved(Filter *filter) { Q_UNUSED(filter) invalidate(); } void SwitchRole::onFiltersCleared() { invalidate(); } } spectral/include/SortFilterProxyModel/CMakeLists.txt0000644000175000000620000000254513566674121022674 0ustar dilingerstaffcmake_minimum_required(VERSION 3.1) set(CMAKE_CXX_STANDARD 11) find_package(Qt5 REQUIRED Core Qml ) set(CMAKE_AUTOMOC ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) # This is to find generated *.moc and *.h files in build dir add_library(SortFilterProxyModel OBJECT qqmlsortfilterproxymodel.cpp filters/filter.cpp filters/filtercontainer.cpp filters/rolefilter.cpp filters/valuefilter.cpp filters/indexfilter.cpp filters/regexpfilter.cpp filters/rangefilter.cpp filters/expressionfilter.cpp filters/filtercontainerfilter.cpp filters/anyoffilter.cpp filters/alloffilter.cpp filters/filtersqmltypes.cpp sorters/sorter.cpp sorters/sortercontainer.cpp sorters/rolesorter.cpp sorters/stringsorter.cpp sorters/expressionsorter.cpp sorters/sortersqmltypes.cpp proxyroles/proxyrole.cpp proxyroles/proxyrolecontainer.cpp proxyroles/joinrole.cpp proxyroles/switchrole.cpp proxyroles/expressionrole.cpp proxyroles/proxyrolesqmltypes.cpp proxyroles/singlerole.cpp proxyroles/regexprole.cpp sorters/filtersorter.cpp proxyroles/filterrole.cpp ) target_include_directories(SortFilterProxyModel PUBLIC ${CMAKE_CURRENT_LIST_DIR} $ $ ) spectral/include/SortFilterProxyModel/filters/0002755000175000000620000000000013566674121021600 5ustar dilingerstaffspectral/include/SortFilterProxyModel/filters/filtersqmltypes.cpp0000644000175000000620000000177513566674121025563 0ustar dilingerstaff#include "filter.h" #include "valuefilter.h" #include "indexfilter.h" #include "regexpfilter.h" #include "rangefilter.h" #include "expressionfilter.h" #include "anyoffilter.h" #include "alloffilter.h" #include #include namespace qqsfpm { void registerFiltersTypes() { qmlRegisterUncreatableType("SortFilterProxyModel", 0, 2, "Filter", "Filter is an abstract class"); qmlRegisterType("SortFilterProxyModel", 0, 2, "ValueFilter"); qmlRegisterType("SortFilterProxyModel", 0, 2, "IndexFilter"); qmlRegisterType("SortFilterProxyModel", 0, 2, "RegExpFilter"); qmlRegisterType("SortFilterProxyModel", 0, 2, "RangeFilter"); qmlRegisterType("SortFilterProxyModel", 0, 2, "ExpressionFilter"); qmlRegisterType("SortFilterProxyModel", 0, 2, "AnyOf"); qmlRegisterType("SortFilterProxyModel", 0, 2, "AllOf"); } Q_COREAPP_STARTUP_FUNCTION(registerFiltersTypes) } spectral/include/SortFilterProxyModel/filters/filter.cpp0000644000175000000620000000343613566674121023575 0ustar dilingerstaff#include "filter.h" #include "qqmlsortfilterproxymodel.h" namespace qqsfpm { /*! \qmltype Filter \inqmlmodule SortFilterProxyModel \brief Base type for the \l SortFilterProxyModel filters The Filter type cannot be used directly in a QML file. It exists to provide a set of common properties and methods, available across all the other filter types that inherit from it. Attempting to use the Filter type directly will result in an error. */ Filter::Filter(QObject *parent) : QObject(parent) { } /*! \qmlproperty bool Filter::enabled This property holds whether the filter is enabled. A disabled filter will accept every rows unconditionally (even if it's inverted). By default, filters are enabled. */ bool Filter::enabled() const { return m_enabled; } void Filter::setEnabled(bool enabled) { if (m_enabled == enabled) return; m_enabled = enabled; Q_EMIT enabledChanged(); Q_EMIT invalidated(); } /*! \qmlproperty bool Filter::inverted This property holds whether the filter is inverted. When a filter is inverted, a row normally accepted would be rejected, and vice-versa. By default, filters are not inverted. */ bool Filter::inverted() const { return m_inverted; } void Filter::setInverted(bool inverted) { if (m_inverted == inverted) return; m_inverted = inverted; Q_EMIT invertedChanged(); invalidate(); } bool Filter::filterAcceptsRow(const QModelIndex &sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const { return !m_enabled || filterRow(sourceIndex, proxyModel) ^ m_inverted; } void Filter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) { Q_UNUSED(proxyModel) } void Filter::invalidate() { if (m_enabled) Q_EMIT invalidated(); } } spectral/include/SortFilterProxyModel/filters/anyoffilter.h0000644000175000000620000000060013566674121024265 0ustar dilingerstaff#ifndef ANYOFFILTER_H #define ANYOFFILTER_H #include "filtercontainerfilter.h" namespace qqsfpm { class AnyOfFilter : public FilterContainerFilter { Q_OBJECT public: using FilterContainerFilter::FilterContainerFilter; protected: bool filterRow(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const override; }; } #endif // ANYOFFILTER_H spectral/include/SortFilterProxyModel/filters/filter.h0000644000175000000620000000204513566674121023235 0ustar dilingerstaff#ifndef FILTER_H #define FILTER_H #include namespace qqsfpm { class QQmlSortFilterProxyModel; class Filter : public QObject { Q_OBJECT Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) Q_PROPERTY(bool inverted READ inverted WRITE setInverted NOTIFY invertedChanged) public: explicit Filter(QObject *parent = nullptr); virtual ~Filter() = default; bool enabled() const; void setEnabled(bool enabled); bool inverted() const; void setInverted(bool inverted); bool filterAcceptsRow(const QModelIndex &sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const; virtual void proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel); Q_SIGNALS: void enabledChanged(); void invertedChanged(); void invalidated(); protected: virtual bool filterRow(const QModelIndex &sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const = 0; void invalidate(); private: bool m_enabled = true; bool m_inverted = false; }; } #endif // FILTER_H spectral/include/SortFilterProxyModel/filters/filtercontainerfilter.h0000644000175000000620000000137013566674121026346 0ustar dilingerstaff#ifndef FILTERCONTAINERFILTER_H #define FILTERCONTAINERFILTER_H #include "filter.h" #include "filtercontainer.h" namespace qqsfpm { class FilterContainerFilter : public Filter, public FilterContainer { Q_OBJECT Q_INTERFACES(qqsfpm::FilterContainer) Q_PROPERTY(QQmlListProperty filters READ filtersListProperty NOTIFY filtersChanged) Q_CLASSINFO("DefaultProperty", "filters") public: using Filter::Filter; void proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) override; Q_SIGNALS: void filtersChanged(); private: void onFilterAppended(Filter* filter) override; void onFilterRemoved(Filter* filter) override; void onFiltersCleared() override; }; } #endif // FILTERCONTAINERFILTER_H spectral/include/SortFilterProxyModel/filters/rangefilter.h0000644000175000000620000000263613566674121024260 0ustar dilingerstaff#ifndef RANGEFILTER_H #define RANGEFILTER_H #include "rolefilter.h" #include namespace qqsfpm { class RangeFilter : public RoleFilter { Q_OBJECT Q_PROPERTY(QVariant minimumValue READ minimumValue WRITE setMinimumValue NOTIFY minimumValueChanged) Q_PROPERTY(bool minimumInclusive READ minimumInclusive WRITE setMinimumInclusive NOTIFY minimumInclusiveChanged) Q_PROPERTY(QVariant maximumValue READ maximumValue WRITE setMaximumValue NOTIFY maximumValueChanged) Q_PROPERTY(bool maximumInclusive READ maximumInclusive WRITE setMaximumInclusive NOTIFY maximumInclusiveChanged) public: using RoleFilter::RoleFilter; QVariant minimumValue() const; void setMinimumValue(QVariant minimumValue); bool minimumInclusive() const; void setMinimumInclusive(bool minimumInclusive); QVariant maximumValue() const; void setMaximumValue(QVariant maximumValue); bool maximumInclusive() const; void setMaximumInclusive(bool maximumInclusive); protected: bool filterRow(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const override; Q_SIGNALS: void minimumValueChanged(); void minimumInclusiveChanged(); void maximumValueChanged(); void maximumInclusiveChanged(); private: QVariant m_minimumValue; bool m_minimumInclusive = true; QVariant m_maximumValue; bool m_maximumInclusive = true; }; } #endif // RANGEFILTER_H spectral/include/SortFilterProxyModel/filters/indexfilter.h0000644000175000000620000000157713566674121024276 0ustar dilingerstaff#ifndef INDEXFILTER_H #define INDEXFILTER_H #include "filter.h" #include namespace qqsfpm { class IndexFilter: public Filter { Q_OBJECT Q_PROPERTY(QVariant minimumIndex READ minimumIndex WRITE setMinimumIndex NOTIFY minimumIndexChanged) Q_PROPERTY(QVariant maximumIndex READ maximumIndex WRITE setMaximumIndex NOTIFY maximumIndexChanged) public: using Filter::Filter; const QVariant& minimumIndex() const; void setMinimumIndex(const QVariant& minimumIndex); const QVariant& maximumIndex() const; void setMaximumIndex(const QVariant& maximumIndex); protected: bool filterRow(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const override; Q_SIGNALS: void minimumIndexChanged(); void maximumIndexChanged(); private: QVariant m_minimumIndex; QVariant m_maximumIndex; }; } #endif // INDEXFILTER_H spectral/include/SortFilterProxyModel/filters/regexpfilter.cpp0000644000175000000620000000627613566674121025015 0ustar dilingerstaff#include "regexpfilter.h" #include namespace qqsfpm { /*! \qmltype RegExpFilter \inherits RoleFilter \inqmlmodule SortFilterProxyModel \brief Filters rows matching a regular expression A RegExpFilter is a \l RoleFilter that accepts rows matching a regular rexpression. In the following example, only rows with their \c lastName role beggining with the content of textfield the will be accepted: \code TextField { id: nameTextField } SortFilterProxyModel { sourceModel: contactModel filters: RegExpFilter { roleName: "lastName" pattern: "^" + nameTextField.displayText } } \endcode */ /*! \qmlproperty bool RegExpFilter::pattern The pattern used to filter the contents of the source model. \sa syntax */ QString RegExpFilter::pattern() const { return m_pattern; } void RegExpFilter::setPattern(const QString& pattern) { if (m_pattern == pattern) return; m_pattern = pattern; m_regExp.setPattern(pattern); Q_EMIT patternChanged(); invalidate(); } /*! \qmlproperty enum RegExpFilter::syntax The pattern used to filter the contents of the source model. Only the source model's value having their \l RoleFilter::roleName data matching this \l pattern with the specified \l syntax will be kept. \value RegExpFilter.RegExp A rich Perl-like pattern matching syntax. This is the default. \value RegExpFilter.Wildcard This provides a simple pattern matching syntax similar to that used by shells (command interpreters) for "file globbing". \value RegExpFilter.FixedString The pattern is a fixed string. This is equivalent to using the RegExp pattern on a string in which all metacharacters are escaped. \value RegExpFilter.RegExp2 Like RegExp, but with greedy quantifiers. \value RegExpFilter.WildcardUnix This is similar to Wildcard but with the behavior of a Unix shell. The wildcard characters can be escaped with the character "\". \value RegExpFilter.W3CXmlSchema11 The pattern is a regular expression as defined by the W3C XML Schema 1.1 specification. \sa pattern */ RegExpFilter::PatternSyntax RegExpFilter::syntax() const { return m_syntax; } void RegExpFilter::setSyntax(RegExpFilter::PatternSyntax syntax) { if (m_syntax == syntax) return; m_syntax = syntax; m_regExp.setPatternSyntax(static_cast(syntax)); Q_EMIT syntaxChanged(); invalidate(); } /*! \qmlproperty Qt::CaseSensitivity RegExpFilter::caseSensitivity This property holds the caseSensitivity of the filter. */ Qt::CaseSensitivity RegExpFilter::caseSensitivity() const { return m_caseSensitivity; } void RegExpFilter::setCaseSensitivity(Qt::CaseSensitivity caseSensitivity) { if (m_caseSensitivity == caseSensitivity) return; m_caseSensitivity = caseSensitivity; m_regExp.setCaseSensitivity(caseSensitivity); Q_EMIT caseSensitivityChanged(); invalidate(); } bool RegExpFilter::filterRow(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const { QString string = sourceData(sourceIndex, proxyModel).toString(); return m_regExp.indexIn(string) != -1; } } spectral/include/SortFilterProxyModel/filters/expressionfilter.cpp0000644000175000000620000001041513566674121025710 0ustar dilingerstaff#include "expressionfilter.h" #include "qqmlsortfilterproxymodel.h" #include namespace qqsfpm { /*! \qmltype ExpressionFilter \inherits Filter \inqmlmodule SortFilterProxyModel \brief Filters row with a custom filtering An ExpressionFilter is a \l Filter allowing to implement custom filtering based on a javascript expression. */ /*! \qmlproperty expression ExpressionFilter::expression An expression to implement custom filtering, it must evaluate to a boolean. It has the same syntax has a \l {http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html} {Property Binding} except it will be evaluated for each of the source model's rows. Rows that have their expression evaluating to \c true will be accepted by the model. Data for each row is exposed like for a delegate of a QML View. This expression is reevaluated for a row every time its model data changes. When an external property (not \c index or in \c model) the expression depends on changes, the expression is reevaluated for every row of the source model. To capture the properties the expression depends on, the expression is first executed with invalid data and each property access is detected by the QML engine. This means that if a property is not accessed because of a conditional, it won't be captured and the expression won't be reevaluted when this property changes. A workaround to this problem is to access all the properties the expressions depends unconditionally at the beggining of the expression. */ const QQmlScriptString& ExpressionFilter::expression() const { return m_scriptString; } void ExpressionFilter::setExpression(const QQmlScriptString& scriptString) { if (m_scriptString == scriptString) return; m_scriptString = scriptString; updateExpression(); Q_EMIT expressionChanged(); invalidate(); } void ExpressionFilter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) { updateContext(proxyModel); } bool ExpressionFilter::filterRow(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const { if (!m_scriptString.isEmpty()) { QVariantMap modelMap; QHash roles = proxyModel.roleNames(); QQmlContext context(qmlContext(this)); auto addToContext = [&] (const QString &name, const QVariant& value) { context.setContextProperty(name, value); modelMap.insert(name, value); }; for (auto it = roles.cbegin(); it != roles.cend(); ++it) addToContext(it.value(), proxyModel.sourceData(sourceIndex, it.key())); addToContext("index", sourceIndex.row()); context.setContextProperty("model", modelMap); QQmlExpression expression(m_scriptString, &context); QVariant variantResult = expression.evaluate(); if (expression.hasError()) { qWarning() << expression.error(); return true; } if (variantResult.canConvert()) { return variantResult.toBool(); } else { qWarning("%s:%i:%i : Can't convert result to bool", expression.sourceFile().toUtf8().data(), expression.lineNumber(), expression.columnNumber()); return true; } } return true; } void ExpressionFilter::updateContext(const QQmlSortFilterProxyModel& proxyModel) { delete m_context; m_context = new QQmlContext(qmlContext(this), this); // what about roles changes ? QVariantMap modelMap; auto addToContext = [&] (const QString &name, const QVariant& value) { m_context->setContextProperty(name, value); modelMap.insert(name, value); }; for (const QByteArray& roleName : proxyModel.roleNames().values()) addToContext(roleName, QVariant()); addToContext("index", -1); m_context->setContextProperty("model", modelMap); updateExpression(); } void ExpressionFilter::updateExpression() { if (!m_context) return; delete m_expression; m_expression = new QQmlExpression(m_scriptString, m_context, 0, this); connect(m_expression, &QQmlExpression::valueChanged, this, &ExpressionFilter::invalidate); m_expression->setNotifyOnValueChanged(true); m_expression->evaluate(); } } spectral/include/SortFilterProxyModel/filters/filtercontainerfilter.cpp0000644000175000000620000000112013566674121026672 0ustar dilingerstaff#include "filtercontainerfilter.h" namespace qqsfpm { void FilterContainerFilter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) { for (Filter* filter : m_filters) filter->proxyModelCompleted(proxyModel); } void FilterContainerFilter::onFilterAppended(Filter* filter) { connect(filter, &Filter::invalidated, this, &FilterContainerFilter::invalidate); invalidate(); } void FilterContainerFilter::onFilterRemoved(Filter* filter) { Q_UNUSED(filter) invalidate(); } void qqsfpm::FilterContainerFilter::onFiltersCleared() { invalidate(); } } spectral/include/SortFilterProxyModel/filters/filtercontainer.h0000644000175000000620000000207113566674121025137 0ustar dilingerstaff#ifndef FILTERCONTAINER_H #define FILTERCONTAINER_H #include #include namespace qqsfpm { class Filter; class QQmlSortFilterProxyModel; class FilterContainer { public: virtual ~FilterContainer(); QList filters() const; void appendFilter(Filter* filter); void removeFilter(Filter* filter); void clearFilters(); QQmlListProperty filtersListProperty(); protected: QList m_filters; private: virtual void onFilterAppended(Filter* filter) = 0; virtual void onFilterRemoved(Filter* filter) = 0; virtual void onFiltersCleared() = 0; static void append_filter(QQmlListProperty* list, Filter* filter); static int count_filter(QQmlListProperty* list); static Filter* at_filter(QQmlListProperty* list, int index); static void clear_filters(QQmlListProperty* list); }; } #define FilterContainer_iid "fr.grecko.SortFilterProxyModel.FilterContainer" Q_DECLARE_INTERFACE(qqsfpm::FilterContainer, FilterContainer_iid) #endif // FILTERCONTAINER_H spectral/include/SortFilterProxyModel/filters/alloffilter.h0000644000175000000620000000060013566674121024246 0ustar dilingerstaff#ifndef ALLOFFILTER_H #define ALLOFFILTER_H #include "filtercontainerfilter.h" namespace qqsfpm { class AllOfFilter : public FilterContainerFilter { Q_OBJECT public: using FilterContainerFilter::FilterContainerFilter; protected: bool filterRow(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const override; }; } #endif // ALLOFFILTER_H spectral/include/SortFilterProxyModel/filters/rolefilter.cpp0000644000175000000620000000215613566674121024455 0ustar dilingerstaff#include "rolefilter.h" #include "qqmlsortfilterproxymodel.h" namespace qqsfpm { /*! \qmltype RoleFilter \qmlabstract \inherits Filter \inqmlmodule SortFilterProxyModel \brief Base type for filters based on a source model role The RoleFilter type cannot be used directly in a QML file. It exists to provide a set of common properties and methods, available across all the other filter types that inherit from it. Attempting to use the RoleFilter type directly will result in an error. */ /*! \qmlproperty string RoleFilter::roleName This property holds the role name that the filter is using to query the source model's data when filtering items. */ const QString& RoleFilter::roleName() const { return m_roleName; } void RoleFilter::setRoleName(const QString& roleName) { if (m_roleName == roleName) return; m_roleName = roleName; Q_EMIT roleNameChanged(); invalidate(); } QVariant RoleFilter::sourceData(const QModelIndex &sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const { return proxyModel.sourceData(sourceIndex, m_roleName); } } spectral/include/SortFilterProxyModel/filters/filtercontainer.cpp0000644000175000000620000000322313566674121025472 0ustar dilingerstaff#include "filtercontainer.h" namespace qqsfpm { FilterContainer::~FilterContainer() { } QList FilterContainer::filters() const { return m_filters; } void FilterContainer::appendFilter(Filter* filter) { m_filters.append(filter); onFilterAppended(filter); } void FilterContainer::removeFilter(Filter* filter) { m_filters.removeOne(filter); onFilterRemoved(filter); } void FilterContainer::clearFilters() { m_filters.clear(); onFiltersCleared(); } QQmlListProperty FilterContainer::filtersListProperty() { return QQmlListProperty(reinterpret_cast(this), &m_filters, &FilterContainer::append_filter, &FilterContainer::count_filter, &FilterContainer::at_filter, &FilterContainer::clear_filters); } void FilterContainer::append_filter(QQmlListProperty* list, Filter* filter) { if (!filter) return; FilterContainer* that = reinterpret_cast(list->object); that->appendFilter(filter); } int FilterContainer::count_filter(QQmlListProperty* list) { QList* filters = static_cast*>(list->data); return filters->count(); } Filter* FilterContainer::at_filter(QQmlListProperty* list, int index) { QList* filters = static_cast*>(list->data); return filters->at(index); } void FilterContainer::clear_filters(QQmlListProperty *list) { FilterContainer* that = reinterpret_cast(list->object); that->clearFilters(); } } spectral/include/SortFilterProxyModel/filters/valuefilter.h0000644000175000000620000000111013566674121024262 0ustar dilingerstaff#ifndef VALUEFILTER_H #define VALUEFILTER_H #include "rolefilter.h" #include namespace qqsfpm { class ValueFilter : public RoleFilter { Q_OBJECT Q_PROPERTY(QVariant value READ value WRITE setValue NOTIFY valueChanged) public: using RoleFilter::RoleFilter; const QVariant& value() const; void setValue(const QVariant& value); protected: bool filterRow(const QModelIndex &sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const override; Q_SIGNALS: void valueChanged(); private: QVariant m_value; }; } #endif // VALUEFILTER_H spectral/include/SortFilterProxyModel/filters/alloffilter.cpp0000644000175000000620000000165113566674121024610 0ustar dilingerstaff#include "alloffilter.h" namespace qqsfpm { /*! \qmltype AllOf \inherits Filter \inqmlmodule SortFilterProxyModel \brief Filter container accepting rows accepted by all its child filters The AllOf type is a \l Filter container that accepts rows if all of its contained (and enabled) filters accept them, or if it has no filter. Using it as a top level filter has the same effect as putting all its child filters as top level filters. It can however be usefull to use an AllOf filter when nested in an AnyOf filter. */ bool AllOfFilter::filterRow(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const { //return true if all filters return false, or if there is no filter. return std::all_of(m_filters.begin(), m_filters.end(), [&sourceIndex, &proxyModel] (Filter* filter) { return filter->filterAcceptsRow(sourceIndex, proxyModel); } ); } } spectral/include/SortFilterProxyModel/filters/anyoffilter.cpp0000644000175000000620000000256713566674121024636 0ustar dilingerstaff#include "anyoffilter.h" namespace qqsfpm { /*! \qmltype AnyOf \inherits Filter \inqmlmodule SortFilterProxyModel \brief Filter container accepting rows accepted by at least one of its child filters The AnyOf type is a \l Filter container that accepts rows if any of its contained (and enabled) filters accept them. In the following example, only the rows where the \c firstName role or the \c lastName role match the text entered in the \c nameTextField will be accepted : \code TextField { id: nameTextField } SortFilterProxyModel { sourceModel: contactModel filters: AnyOf { RegExpFilter { roleName: "lastName" pattern: nameTextField.text caseSensitivity: Qt.CaseInsensitive } RegExpFilter { roleName: "firstName" pattern: nameTextField.text caseSensitivity: Qt.CaseInsensitive } } } \endcode */ bool AnyOfFilter::filterRow(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const { //return true if any of the enabled filters return true return std::any_of(m_filters.begin(), m_filters.end(), [&sourceIndex, &proxyModel] (Filter* filter) { return filter->enabled() && filter->filterAcceptsRow(sourceIndex, proxyModel); } ); } } spectral/include/SortFilterProxyModel/filters/rangefilter.cpp0000644000175000000620000000635513566674121024615 0ustar dilingerstaff#include "rangefilter.h" namespace qqsfpm { /*! \qmltype RangeFilter \inherits RoleFilter \inqmlmodule SortFilterProxyModel \brief Filters rows between boundary values A RangeFilter is a \l RoleFilter that accepts rows if their data is between the filter's minimum and maximum value. In the following example, only rows with their \c price role set to a value between the tow boundary of the slider will be accepted : \code RangeSlider { id: priceRangeSlider } SortFilterProxyModel { sourceModel: priceModel filters: RangeFilter { roleName: "price" minimumValue: priceRangeSlider.first.value maximumValue: priceRangeSlider.second.value } } \endcode */ /*! \qmlproperty int RangeFilter::minimumValue This property holds the minimumValue of the filter. Rows with a value lower than \c minimumValue will be rejected. By default, no value is set. \sa minimumInclusive */ QVariant RangeFilter::minimumValue() const { return m_minimumValue; } void RangeFilter::setMinimumValue(QVariant minimumValue) { if (m_minimumValue == minimumValue) return; m_minimumValue = minimumValue; Q_EMIT minimumValueChanged(); invalidate(); } /*! \qmlproperty int RangeFilter::minimumInclusive This property holds whether the \l minimumValue is inclusive. By default, the \l minimumValue is inclusive. \sa minimumValue */ bool RangeFilter::minimumInclusive() const { return m_minimumInclusive; } void RangeFilter::setMinimumInclusive(bool minimumInclusive) { if (m_minimumInclusive == minimumInclusive) return; m_minimumInclusive = minimumInclusive; Q_EMIT minimumInclusiveChanged(); invalidate(); } /*! \qmlproperty int RangeFilter::maximumValue This property holds the maximumValue of the filter. Rows with a value higher than \c maximumValue will be rejected. By default, no value is set. \sa maximumInclusive */ QVariant RangeFilter::maximumValue() const { return m_maximumValue; } void RangeFilter::setMaximumValue(QVariant maximumValue) { if (m_maximumValue == maximumValue) return; m_maximumValue = maximumValue; Q_EMIT maximumValueChanged(); invalidate(); } /*! \qmlproperty int RangeFilter::maximumInclusive This property holds whether the \l minimumValue is inclusive. By default, the \l minimumValue is inclusive. \sa minimumValue */ bool RangeFilter::maximumInclusive() const { return m_maximumInclusive; } void RangeFilter::setMaximumInclusive(bool maximumInclusive) { if (m_maximumInclusive == maximumInclusive) return; m_maximumInclusive = maximumInclusive; Q_EMIT maximumInclusiveChanged(); invalidate(); } bool RangeFilter::filterRow(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const { QVariant value = sourceData(sourceIndex, proxyModel); bool lessThanMin = m_minimumValue.isValid() && (m_minimumInclusive ? value < m_minimumValue : value <= m_minimumValue); bool moreThanMax = m_maximumValue.isValid() && (m_maximumInclusive ? value > m_maximumValue : value >= m_maximumValue); return !(lessThanMin || moreThanMax); } } spectral/include/SortFilterProxyModel/filters/expressionfilter.h0000644000175000000620000000170413566674121025356 0ustar dilingerstaff#ifndef EXPRESSIONFILTER_H #define EXPRESSIONFILTER_H #include "filter.h" #include class QQmlExpression; namespace qqsfpm { class ExpressionFilter : public Filter { Q_OBJECT Q_PROPERTY(QQmlScriptString expression READ expression WRITE setExpression NOTIFY expressionChanged) public: using Filter::Filter; const QQmlScriptString& expression() const; void setExpression(const QQmlScriptString& scriptString); void proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel) override; protected: bool filterRow(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const override; Q_SIGNALS: void expressionChanged(); private: void updateContext(const QQmlSortFilterProxyModel& proxyModel); void updateExpression(); QQmlScriptString m_scriptString; QQmlExpression* m_expression = nullptr; QQmlContext* m_context = nullptr; }; } #endif // EXPRESSIONFILTER_H spectral/include/SortFilterProxyModel/filters/regexpfilter.h0000644000175000000620000000301013566674121024441 0ustar dilingerstaff#ifndef REGEXPFILTER_H #define REGEXPFILTER_H #include "rolefilter.h" namespace qqsfpm { class RegExpFilter : public RoleFilter { Q_OBJECT Q_PROPERTY(QString pattern READ pattern WRITE setPattern NOTIFY patternChanged) Q_PROPERTY(PatternSyntax syntax READ syntax WRITE setSyntax NOTIFY syntaxChanged) Q_PROPERTY(Qt::CaseSensitivity caseSensitivity READ caseSensitivity WRITE setCaseSensitivity NOTIFY caseSensitivityChanged) public: enum PatternSyntax { RegExp = QRegExp::RegExp, Wildcard = QRegExp::Wildcard, FixedString = QRegExp::FixedString, RegExp2 = QRegExp::RegExp2, WildcardUnix = QRegExp::WildcardUnix, W3CXmlSchema11 = QRegExp::W3CXmlSchema11 }; Q_ENUMS(PatternSyntax) using RoleFilter::RoleFilter; QString pattern() const; void setPattern(const QString& pattern); PatternSyntax syntax() const; void setSyntax(PatternSyntax syntax); Qt::CaseSensitivity caseSensitivity() const; void setCaseSensitivity(Qt::CaseSensitivity caseSensitivity); protected: bool filterRow(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const override; Q_SIGNALS: void patternChanged(); void syntaxChanged(); void caseSensitivityChanged(); private: QRegExp m_regExp; Qt::CaseSensitivity m_caseSensitivity = m_regExp.caseSensitivity(); PatternSyntax m_syntax = static_cast(m_regExp.patternSyntax()); QString m_pattern = m_regExp.pattern(); }; } #endif // REGEXPFILTER_H spectral/include/SortFilterProxyModel/filters/valuefilter.cpp0000644000175000000620000000240213566674121024622 0ustar dilingerstaff#include "valuefilter.h" namespace qqsfpm { /*! \qmltype ValueFilter \inherits RoleFilter \inqmlmodule SortFilterProxyModel \brief Filters rows matching exactly a value A ValueFilter is a simple \l RoleFilter that accepts rows matching exactly the filter's value In the following example, only rows with their \c favorite role set to \c true will be accepted when the checkbox is checked : \code CheckBox { id: showOnlyFavoriteCheckBox } SortFilterProxyModel { sourceModel: contactModel filters: ValueFilter { roleName: "favorite" value: true enabled: showOnlyFavoriteCheckBox.checked } } \endcode */ /*! \qmlproperty variant ValueFilter::value This property holds the value used to filter the contents of the source model. */ const QVariant &ValueFilter::value() const { return m_value; } void ValueFilter::setValue(const QVariant& value) { if (m_value == value) return; m_value = value; Q_EMIT valueChanged(); invalidate(); } bool ValueFilter::filterRow(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const { return !m_value.isValid() || m_value == sourceData(sourceIndex, proxyModel); } } spectral/include/SortFilterProxyModel/filters/rolefilter.h0000644000175000000620000000106313566674121024116 0ustar dilingerstaff#ifndef ROLEFILTER_H #define ROLEFILTER_H #include "filter.h" namespace qqsfpm { class RoleFilter : public Filter { Q_OBJECT Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged) public: using Filter::Filter; const QString& roleName() const; void setRoleName(const QString& roleName); Q_SIGNALS: void roleNameChanged(); protected: QVariant sourceData(const QModelIndex &sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const; private: QString m_roleName; }; } #endif // ROLEFILTER_H spectral/include/SortFilterProxyModel/filters/indexfilter.cpp0000644000175000000620000000532613566674121024625 0ustar dilingerstaff#include "indexfilter.h" #include "qqmlsortfilterproxymodel.h" namespace qqsfpm { /*! \qmltype IndexFilter \inherits Filter \inqmlmodule SortFilterProxyModel \brief Filters rows based on their source index An IndexFilter is a filter allowing contents to be filtered based on their source model index. In the following example, only the first row of the source model will be accepted: \code SortFilterProxyModel { sourceModel: contactModel filters: IndexFilter { maximumIndex: 0 } } \endcode */ /*! \qmlproperty int IndexFilter::minimumIndex This property holds the minimumIndex of the filter. Rows with a source index lower than \c minimumIndex will be rejected. If \c minimumIndex is negative, it is counted from the end of the source model, meaning that : \code minimumIndex: -1\endcode is equivalent to : \code minimumIndex: sourceModel.count - 1\endcode By default, no value is set. */ const QVariant& IndexFilter::minimumIndex() const { return m_minimumIndex; } void IndexFilter::setMinimumIndex(const QVariant& minimumIndex) { if (m_minimumIndex == minimumIndex) return; m_minimumIndex = minimumIndex; Q_EMIT minimumIndexChanged(); invalidate(); } /*! \qmlproperty int IndexFilter::maximumIndex This property holds the maximumIndex of the filter. Rows with a source index higher than \c maximumIndex will be rejected. If \c maximumIndex is negative, it is counted from the end of the source model, meaning that: \code maximumIndex: -1\endcode is equivalent to : \code maximumIndex: sourceModel.count - 1\endcode By default, no value is set. */ const QVariant& IndexFilter::maximumIndex() const { return m_maximumIndex; } void IndexFilter::setMaximumIndex(const QVariant& maximumIndex) { if (m_maximumIndex == maximumIndex) return; m_maximumIndex = maximumIndex; Q_EMIT maximumIndexChanged(); invalidate(); } bool IndexFilter::filterRow(const QModelIndex& sourceIndex, const QQmlSortFilterProxyModel& proxyModel) const { int sourceRowCount = proxyModel.sourceModel()->rowCount(); int sourceRow = sourceIndex.row(); bool minimumIsValid; int minimum = m_minimumIndex.toInt(&minimumIsValid); if (minimumIsValid) { int actualMinimum = minimum < 0 ? sourceRowCount + minimum : minimum; if (sourceRow < actualMinimum) return false; } bool maximumIsValid; int maximum = m_maximumIndex.toInt(&maximumIsValid); if (maximumIsValid) { int actualMaximum = maximum < 0 ? sourceRowCount + maximum : maximum; if (sourceRow > actualMaximum) return false; } return true; } } spectral/include/SortFilterProxyModel/sortfilterproxymodel.qdocconf0000644000175000000620000000057213566674121026170 0ustar dilingerstaffproject = SortFilterProxyModel description = lol sourcedirs = . sources.fileextensions = "*.cpp *.qdoc *.qml" headers.fileextensions = "*.h" outputdir = docs/ HTML.templatedir = . HTML.stylesheets = "D:\\coding\\Qt\\Docs\\Qt-5.8\\global\\template\\style\\offline.css" HTML.headerstyles = \ " \n" spectral/include/SortFilterProxyModel/qqmlsortfilterproxymodel.cpp0000644000175000000620000003660313566674121026055 0ustar dilingerstaff#include "qqmlsortfilterproxymodel.h" #include #include #include "filters/filter.h" #include "sorters/sorter.h" #include "proxyroles/proxyrole.h" namespace qqsfpm { /*! \page index.html overview \title SortFilterProxyModel QML Module SortFilterProxyModel is an implementation of QSortFilterProxyModel conveniently exposed for QML. \generatelist qmltypesbymodule SortFilterProxyModel */ /*! \qmltype SortFilterProxyModel \inqmlmodule SortFilterProxyModel \brief Filters and sorts data coming from a source \l {http://doc.qt.io/qt-5/qabstractitemmodel.html} {QAbstractItemModel} The SortFilterProxyModel type provides support for filtering and sorting data coming from a source model. */ QQmlSortFilterProxyModel::QQmlSortFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) { connect(this, &QAbstractProxyModel::sourceModelChanged, this, &QQmlSortFilterProxyModel::updateRoles); connect(this, &QAbstractItemModel::modelReset, this, &QQmlSortFilterProxyModel::updateRoles); connect(this, &QAbstractItemModel::rowsInserted, this, &QQmlSortFilterProxyModel::countChanged); connect(this, &QAbstractItemModel::rowsRemoved, this, &QQmlSortFilterProxyModel::countChanged); connect(this, &QAbstractItemModel::modelReset, this, &QQmlSortFilterProxyModel::countChanged); connect(this, &QAbstractItemModel::layoutChanged, this, &QQmlSortFilterProxyModel::countChanged); connect(this, &QAbstractItemModel::dataChanged, this, &QQmlSortFilterProxyModel::onDataChanged); setDynamicSortFilter(true); } /*! \qmlproperty QAbstractItemModel* SortFilterProxyModel::sourceModel The source model of this proxy model */ /*! \qmlproperty int SortFilterProxyModel::count The number of rows in the proxy model (not filtered out the source model) */ int QQmlSortFilterProxyModel::count() const { return rowCount(); } const QString& QQmlSortFilterProxyModel::filterRoleName() const { return m_filterRoleName; } void QQmlSortFilterProxyModel::setFilterRoleName(const QString& filterRoleName) { if (m_filterRoleName == filterRoleName) return; m_filterRoleName = filterRoleName; updateFilterRole(); Q_EMIT filterRoleNameChanged(); } QString QQmlSortFilterProxyModel::filterPattern() const { return filterRegExp().pattern(); } void QQmlSortFilterProxyModel::setFilterPattern(const QString& filterPattern) { QRegExp regExp = filterRegExp(); if (regExp.pattern() == filterPattern) return; regExp.setPattern(filterPattern); QSortFilterProxyModel::setFilterRegExp(regExp); Q_EMIT filterPatternChanged(); } QQmlSortFilterProxyModel::PatternSyntax QQmlSortFilterProxyModel::filterPatternSyntax() const { return static_cast(filterRegExp().patternSyntax()); } void QQmlSortFilterProxyModel::setFilterPatternSyntax(QQmlSortFilterProxyModel::PatternSyntax patternSyntax) { QRegExp regExp = filterRegExp(); QRegExp::PatternSyntax patternSyntaxTmp = static_cast(patternSyntax); if (regExp.patternSyntax() == patternSyntaxTmp) return; regExp.setPatternSyntax(patternSyntaxTmp); QSortFilterProxyModel::setFilterRegExp(regExp); Q_EMIT filterPatternSyntaxChanged(); } const QVariant& QQmlSortFilterProxyModel::filterValue() const { return m_filterValue; } void QQmlSortFilterProxyModel::setFilterValue(const QVariant& filterValue) { if (m_filterValue == filterValue) return; m_filterValue = filterValue; invalidateFilter(); Q_EMIT filterValueChanged(); } /*! \qmlproperty string SortFilterProxyModel::sortRoleName The role name of the source model's data used for the sorting. \sa {http://doc.qt.io/qt-5/qsortfilterproxymodel.html#sortRole-prop} {sortRole}, roleForName */ const QString& QQmlSortFilterProxyModel::sortRoleName() const { return m_sortRoleName; } void QQmlSortFilterProxyModel::setSortRoleName(const QString& sortRoleName) { if (m_sortRoleName == sortRoleName) return; m_sortRoleName = sortRoleName; updateSortRole(); Q_EMIT sortRoleNameChanged(); } bool QQmlSortFilterProxyModel::ascendingSortOrder() const { return m_ascendingSortOrder; } void QQmlSortFilterProxyModel::setAscendingSortOrder(bool ascendingSortOrder) { if (m_ascendingSortOrder == ascendingSortOrder) return; m_ascendingSortOrder = ascendingSortOrder; Q_EMIT ascendingSortOrderChanged(); invalidate(); } /*! \qmlproperty list SortFilterProxyModel::filters This property holds the list of filters for this proxy model. To be included in the model, a row of the source model has to be accepted by all the top level filters of this list. \sa Filter */ /*! \qmlproperty list SortFilterProxyModel::sorters This property holds the list of sorters for this proxy model. The rows of the source model are sorted by the sorters of this list, in their order of insertion. \sa Sorter */ /*! \qmlproperty list SortFilterProxyModel::proxyRoles This property holds the list of proxy roles for this proxy model. Each proxy role adds a new custom role to the model. \sa ProxyRole */ void QQmlSortFilterProxyModel::classBegin() { } void QQmlSortFilterProxyModel::componentComplete() { m_completed = true; for (const auto& filter : m_filters) filter->proxyModelCompleted(*this); for (const auto& sorter : m_sorters) sorter->proxyModelCompleted(*this); for (const auto& proxyRole : m_proxyRoles) proxyRole->proxyModelCompleted(*this); invalidate(); sort(0); } QVariant QQmlSortFilterProxyModel::sourceData(const QModelIndex& sourceIndex, const QString& roleName) const { int role = roleNames().key(roleName.toUtf8()); return sourceData(sourceIndex, role); } QVariant QQmlSortFilterProxyModel::sourceData(const QModelIndex &sourceIndex, int role) const { QPair proxyRolePair = m_proxyRoleMap[role]; if (ProxyRole* proxyRole = proxyRolePair.first) return proxyRole->roleData(sourceIndex, *this, proxyRolePair.second); else return sourceModel()->data(sourceIndex, role); } QVariant QQmlSortFilterProxyModel::data(const QModelIndex &index, int role) const { return sourceData(mapToSource(index), role); } QHash QQmlSortFilterProxyModel::roleNames() const { return m_roleNames.isEmpty() && sourceModel() ? sourceModel()->roleNames() : m_roleNames; } /*! \qmlmethod int SortFilterProxyModel::roleForName(string roleName) Returns the role number for the given \a roleName. If no role is found for this \a roleName, \c -1 is returned. */ int QQmlSortFilterProxyModel::roleForName(const QString& roleName) const { return m_roleNames.key(roleName.toUtf8(), -1); } /*! \qmlmethod object SortFilterProxyModel::get(int row) Return the item at \a row in the proxy model as a map of all its roles. This allows the item data to be read (not modified) from JavaScript. */ QVariantMap QQmlSortFilterProxyModel::get(int row) const { QVariantMap map; QModelIndex modelIndex = index(row, 0); QHash roles = roleNames(); for (QHash::const_iterator it = roles.begin(); it != roles.end(); ++it) map.insert(it.value(), data(modelIndex, it.key())); return map; } /*! \qmlmethod variant SortFilterProxyModel::get(int row, string roleName) Return the data for the given \a roleNamte of the item at \a row in the proxy model. This allows the role data to be read (not modified) from JavaScript. This equivalent to calling \c {data(index(row, 0), roleForName(roleName))}. */ QVariant QQmlSortFilterProxyModel::get(int row, const QString& roleName) const { return data(index(row, 0), roleForName(roleName)); } /*! \qmlmethod index SortFilterProxyModel::mapToSource(index proxyIndex) Returns the source model index corresponding to the given \a proxyIndex from the SortFilterProxyModel. */ QModelIndex QQmlSortFilterProxyModel::mapToSource(const QModelIndex& proxyIndex) const { return QSortFilterProxyModel::mapToSource(proxyIndex); } /*! \qmlmethod int SortFilterProxyModel::mapToSource(int proxyRow) Returns the source model row corresponding to the given \a proxyRow from the SortFilterProxyModel. Returns -1 if there is no corresponding row. */ int QQmlSortFilterProxyModel::mapToSource(int proxyRow) const { QModelIndex proxyIndex = index(proxyRow, 0); QModelIndex sourceIndex = mapToSource(proxyIndex); return sourceIndex.isValid() ? sourceIndex.row() : -1; } /*! \qmlmethod QModelIndex SortFilterProxyModel::mapFromSource(QModelIndex sourceIndex) Returns the model index in the SortFilterProxyModel given the sourceIndex from the source model. */ QModelIndex QQmlSortFilterProxyModel::mapFromSource(const QModelIndex& sourceIndex) const { return QSortFilterProxyModel::mapFromSource(sourceIndex); } /*! \qmlmethod int SortFilterProxyModel::mapFromSource(int sourceRow) Returns the row in the SortFilterProxyModel given the \a sourceRow from the source model. Returns -1 if there is no corresponding row. */ int QQmlSortFilterProxyModel::mapFromSource(int sourceRow) const { QModelIndex proxyIndex; if (QAbstractItemModel* source = sourceModel()) { QModelIndex sourceIndex = source->index(sourceRow, 0); proxyIndex = mapFromSource(sourceIndex); } return proxyIndex.isValid() ? proxyIndex.row() : -1; } bool QQmlSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { if (!m_completed) return true; QModelIndex sourceIndex = sourceModel()->index(source_row, 0, source_parent); bool valueAccepted = !m_filterValue.isValid() || ( m_filterValue == sourceModel()->data(sourceIndex, filterRole()) ); bool baseAcceptsRow = valueAccepted && QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); baseAcceptsRow = baseAcceptsRow && std::all_of(m_filters.begin(), m_filters.end(), [=, &source_parent] (Filter* filter) { return filter->filterAcceptsRow(sourceIndex, *this); } ); return baseAcceptsRow; } bool QQmlSortFilterProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const { if (m_completed) { if (!m_sortRoleName.isEmpty()) { if (QSortFilterProxyModel::lessThan(source_left, source_right)) return m_ascendingSortOrder; if (QSortFilterProxyModel::lessThan(source_right, source_left)) return !m_ascendingSortOrder; } for(auto sorter : m_sorters) { if (sorter->enabled()) { int comparison = sorter->compareRows(source_left, source_right, *this); if (comparison != 0) return comparison < 0; } } } return source_left.row() < source_right.row(); } void QQmlSortFilterProxyModel::resetInternalData() { QSortFilterProxyModel::resetInternalData(); updateRoleNames(); } void QQmlSortFilterProxyModel::setSourceModel(QAbstractItemModel *sourceModel) { if (sourceModel && sourceModel->roleNames().isEmpty()) { // workaround for when a model has no roles and roles are added when the model is populated (ListModel) // QTBUG-57971 connect(sourceModel, &QAbstractItemModel::rowsInserted, this, &QQmlSortFilterProxyModel::initRoles); } QSortFilterProxyModel::setSourceModel(sourceModel); } void QQmlSortFilterProxyModel::invalidateFilter() { if (m_completed) QSortFilterProxyModel::invalidateFilter(); } void QQmlSortFilterProxyModel::invalidate() { if (m_completed) QSortFilterProxyModel::invalidate(); } void QQmlSortFilterProxyModel::updateRoleNames() { if (!sourceModel()) return; m_roleNames = sourceModel()->roleNames(); m_proxyRoleMap.clear(); m_proxyRoleNumbers.clear(); auto roles = m_roleNames.keys(); auto maxIt = std::max_element(roles.cbegin(), roles.cend()); int maxRole = maxIt != roles.cend() ? *maxIt : -1; for (auto proxyRole : m_proxyRoles) { for (auto roleName : proxyRole->names()) { ++maxRole; m_roleNames[maxRole] = roleName.toUtf8(); m_proxyRoleMap[maxRole] = {proxyRole, roleName}; m_proxyRoleNumbers.append(maxRole); } } } void QQmlSortFilterProxyModel::updateFilterRole() { QList filterRoles = roleNames().keys(m_filterRoleName.toUtf8()); if (!filterRoles.empty()) { setFilterRole(filterRoles.first()); } } void QQmlSortFilterProxyModel::updateSortRole() { QList sortRoles = roleNames().keys(m_sortRoleName.toUtf8()); if (!sortRoles.empty()) { setSortRole(sortRoles.first()); invalidate(); } } void QQmlSortFilterProxyModel::updateRoles() { updateFilterRole(); updateSortRole(); } void QQmlSortFilterProxyModel::initRoles() { disconnect(sourceModel(), &QAbstractItemModel::rowsInserted, this, &QQmlSortFilterProxyModel::initRoles); resetInternalData(); updateRoles(); } void QQmlSortFilterProxyModel::onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles) { Q_UNUSED(roles); if (!roles.isEmpty() && !m_proxyRoleNumbers.empty() && roles != m_proxyRoleNumbers) Q_EMIT dataChanged(topLeft, bottomRight, m_proxyRoleNumbers); } void QQmlSortFilterProxyModel::emitProxyRolesChanged() { invalidate(); Q_EMIT dataChanged(index(0,0), index(rowCount() - 1, columnCount() - 1), m_proxyRoleNumbers); } QVariantMap QQmlSortFilterProxyModel::modelDataMap(const QModelIndex& modelIndex) const { QVariantMap map; QHash roles = roleNames(); for (QHash::const_iterator it = roles.begin(); it != roles.end(); ++it) map.insert(it.value(), sourceModel()->data(modelIndex, it.key())); return map; } void QQmlSortFilterProxyModel::onFilterAppended(Filter* filter) { connect(filter, &Filter::invalidated, this, &QQmlSortFilterProxyModel::invalidateFilter); this->invalidateFilter(); } void QQmlSortFilterProxyModel::onFilterRemoved(Filter* filter) { Q_UNUSED(filter) invalidateFilter(); } void QQmlSortFilterProxyModel::onFiltersCleared() { invalidateFilter(); } void QQmlSortFilterProxyModel::onSorterAppended(Sorter* sorter) { connect(sorter, &Sorter::invalidated, this, &QQmlSortFilterProxyModel::invalidate); invalidate(); } void QQmlSortFilterProxyModel::onSorterRemoved(Sorter* sorter) { Q_UNUSED(sorter) invalidate(); } void QQmlSortFilterProxyModel::onSortersCleared() { invalidate(); } void QQmlSortFilterProxyModel::onProxyRoleAppended(ProxyRole *proxyRole) { beginResetModel(); connect(proxyRole, &ProxyRole::invalidated, this, &QQmlSortFilterProxyModel::emitProxyRolesChanged); connect(proxyRole, &ProxyRole::namesAboutToBeChanged, this, &QQmlSortFilterProxyModel::beginResetModel); connect(proxyRole, &ProxyRole::namesChanged, this, &QQmlSortFilterProxyModel::endResetModel); endResetModel(); } void QQmlSortFilterProxyModel::onProxyRoleRemoved(ProxyRole *proxyRole) { Q_UNUSED(proxyRole) beginResetModel(); endResetModel(); } void QQmlSortFilterProxyModel::onProxyRolesCleared() { beginResetModel(); endResetModel(); } void registerQQmlSortFilterProxyModelTypes() { qmlRegisterType("SortFilterProxyModel", 0, 2, "SortFilterProxyModel"); } Q_COREAPP_STARTUP_FUNCTION(registerQQmlSortFilterProxyModelTypes) } spectral/include/SortFilterProxyModel/LICENSE0000644000175000000620000000207213566674121021134 0ustar dilingerstaffThe MIT License (MIT) Copyright (c) 2016 Pierre-Yves Siret Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.spectral/include/libQuotient/0002755000175000000620000000000013566676054016316 5ustar dilingerstaffspectral/include/libQuotient/cmake/0002755000175000000620000000000013566674122017370 5ustar dilingerstaffspectral/include/libQuotient/cmake/QuotientConfig.cmake0000644000175000000620000000016513566674122023330 0ustar dilingerstaffinclude(CMakeFindDependencyMacro) find_dependency(QtOlm) include("${CMAKE_CURRENT_LIST_DIR}/QuotientTargets.cmake") spectral/include/libQuotient/libquotient.pri0000644000175000000620000000751113566674122021365 0ustar dilingerstaffQT += network multimedia # TODO: Having moved to Qt 5.12, replace c++1z with c++17 below CONFIG *= c++1z warn_on rtti_off create_prl object_parallel_to_source win32-msvc* { QMAKE_CXXFLAGS_WARN_ON += -wd4100 -wd4267 } else { QMAKE_CXXFLAGS_WARN_ON += -Wno-unused-parameter } include(3rdparty/libQtOlm/libQtOlm.pri) SRCPATH = $$PWD/lib INCLUDEPATH += $$SRCPATH HEADERS += \ $$SRCPATH/connectiondata.h \ $$SRCPATH/connection.h \ $$SRCPATH/encryptionmanager.h \ $$SRCPATH/eventitem.h \ $$SRCPATH/room.h \ $$SRCPATH/user.h \ $$SRCPATH/avatar.h \ $$SRCPATH/syncdata.h \ $$SRCPATH/util.h \ $$SRCPATH/qt_connection_util.h \ $$SRCPATH/events/event.h \ $$SRCPATH/events/roomevent.h \ $$SRCPATH/events/stateevent.h \ $$SRCPATH/events/eventcontent.h \ $$SRCPATH/events/roommessageevent.h \ $$SRCPATH/events/simplestateevents.h \ $$SRCPATH/events/roomcreateevent.h \ $$SRCPATH/events/roomtombstoneevent.h \ $$SRCPATH/events/roommemberevent.h \ $$SRCPATH/events/roomavatarevent.h \ $$SRCPATH/events/typingevent.h \ $$SRCPATH/events/receiptevent.h \ $$SRCPATH/events/reactionevent.h \ $$SRCPATH/events/callanswerevent.h \ $$SRCPATH/events/callcandidatesevent.h \ $$SRCPATH/events/callhangupevent.h \ $$SRCPATH/events/callinviteevent.h \ $$SRCPATH/events/accountdataevents.h \ $$SRCPATH/events/directchatevent.h \ $$SRCPATH/events/encryptionevent.h \ $$SRCPATH/events/encryptedevent.h \ $$SRCPATH/events/redactionevent.h \ $$SRCPATH/events/eventloader.h \ $$SRCPATH/jobs/requestdata.h \ $$SRCPATH/jobs/basejob.h \ $$SRCPATH/jobs/syncjob.h \ $$SRCPATH/jobs/mediathumbnailjob.h \ $$SRCPATH/jobs/downloadfilejob.h \ $$SRCPATH/jobs/postreadmarkersjob.h \ $$files($$SRCPATH/csapi/*.h, false) \ $$files($$SRCPATH/csapi/definitions/*.h, false) \ $$files($$SRCPATH/csapi/definitions/wellknown/*.h, false) \ $$files($$SRCPATH/application-service/definitions/*.h, false) \ $$files($$SRCPATH/identity/definitions/*.h, false) \ $$SRCPATH/logging.h \ $$SRCPATH/converters.h \ $$SRCPATH/settings.h \ $$SRCPATH/networksettings.h \ $$SRCPATH/networkaccessmanager.h SOURCES += \ $$SRCPATH/connectiondata.cpp \ $$SRCPATH/connection.cpp \ $$SRCPATH/encryptionmanager.cpp \ $$SRCPATH/eventitem.cpp \ $$SRCPATH/room.cpp \ $$SRCPATH/user.cpp \ $$SRCPATH/avatar.cpp \ $$SRCPATH/syncdata.cpp \ $$SRCPATH/util.cpp \ $$SRCPATH/events/event.cpp \ $$SRCPATH/events/roomevent.cpp \ $$SRCPATH/events/stateevent.cpp \ $$SRCPATH/events/eventcontent.cpp \ $$SRCPATH/events/roomcreateevent.cpp \ $$SRCPATH/events/roomtombstoneevent.cpp \ $$SRCPATH/events/roommessageevent.cpp \ $$SRCPATH/events/roommemberevent.cpp \ $$SRCPATH/events/typingevent.cpp \ $$SRCPATH/events/reactionevent.cpp \ $$SRCPATH/events/callanswerevent.cpp \ $$SRCPATH/events/callcandidatesevent.cpp \ $$SRCPATH/events/callhangupevent.cpp \ $$SRCPATH/events/callinviteevent.cpp \ $$SRCPATH/events/receiptevent.cpp \ $$SRCPATH/events/directchatevent.cpp \ $$SRCPATH/events/encryptionevent.cpp \ $$SRCPATH/events/encryptedevent.cpp \ $$SRCPATH/jobs/requestdata.cpp \ $$SRCPATH/jobs/basejob.cpp \ $$SRCPATH/jobs/syncjob.cpp \ $$SRCPATH/jobs/mediathumbnailjob.cpp \ $$SRCPATH/jobs/downloadfilejob.cpp \ $$files($$SRCPATH/csapi/*.cpp, false) \ $$files($$SRCPATH/csapi/definitions/*.cpp, false) \ $$files($$SRCPATH/csapi/definitions/wellknown/*.cpp, false) \ $$files($$SRCPATH/application-service/definitions/*.cpp, false) \ $$files($$SRCPATH/identity/definitions/*.cpp, false) \ $$SRCPATH/logging.cpp \ $$SRCPATH/converters.cpp \ $$SRCPATH/settings.cpp \ $$SRCPATH/networksettings.cpp \ $$SRCPATH/networkaccessmanager.cpp spectral/include/libQuotient/README.md0000644000175000000620000002416613566674122017576 0ustar dilingerstaff# libQuotient (former libQMatrixClient) Made for Matrix [![license](https://img.shields.io/github/license/quotient-im/libQuotient.svg)](https://github.com/quotient-im/libQuotient/blob/master/COPYING) ![status](https://img.shields.io/badge/status-beta-yellow.svg) [![release](https://img.shields.io/github/release/quotient-im/libQuotient/all.svg)](https://github.com/quotient-im/libQuotient/releases/latest) [![](https://img.shields.io/cii/percentage/1023.svg?label=CII%20best%20practices)](https://bestpractices.coreinfrastructure.org/projects/1023/badge) ![](https://img.shields.io/github/commit-activity/y/quotient-im/libQuotient.svg) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) The Quotient project aims to produce a Qt5-based SDK to develop applications for [Matrix](https://matrix.org). libQuotient is a library that enables client applications. It is the backbone of [Quaternion](https://github.com/quotient-im/Quaternion), [Spectral](https://matrix.org/docs/projects/client/spectral.html) and other projects. Versions 0.5.x and older use the previous name - libQMatrixClient. ## Contacts You can find Quotient developers in the Matrix room: [#quotient:matrix.org](https://matrix.to/#/#quotient:matrix.org). You can file issues at [the project issue tracker](https://github.com/quotient-im/libQuotient/issues). If you find what looks like a security issue, please use instructions in SECURITY.md. ## Getting and using libQuotient Depending on your platform, the library can come as a separate package. Recent releases of Debian and openSUSE, e.g., already have the package (under the old name). If your Linux repo doesn't provide binary package (either libqmatrixclient - older - or libquotient - newer), or you're on Windows or macOS, your best bet is to build the library from the source and bundle it with your application. ### Pre-requisites - A recent Linux, macOS or Windows system (desktop versions tried; Ubuntu Touch is known to work; mobile Windows and iOS might work too but never tried) - Recent enough Linux examples: Debian Buster; Fedora 28; openSUSE Leap 15; Ubuntu Bionic Beaver. - Qt 5 (either Open Source or Commercial), 5.9 or higher; 5.12 is recommended, especially if you use qmake - A build configuration tool (CMake is recommended, qmake is yet supported): - CMake 3.10 or newer (from your package management system or [the official website](https://cmake.org/download/)) - or qmake (comes with Qt) - A C++ toolchain with C++17 support: - GCC 7 (Windows, Linux, macOS), Clang 6 (Linux), Apple Clang 10 (macOS) and Visual Studio 2017 (Windows) are the oldest officially supported. - Any build system that works with CMake and/or qmake should be fine: GNU Make, ninja (any platform), NMake, jom (Windows) are known to work. #### Linux Just install things from the list above using your preferred package manager. If your Qt package base is fine-grained you might want to run cmake/qmake and look at error messages. The library is entirely offscreen (QtCore and QtNetwork are essential) but it also depends on QtGui in order to handle avatar thumbnails. #### macOS `brew install qt5` should get you a recent Qt5. If you plan to use CMake, you will need to tell it about the path to Qt by passing `-DCMAKE_PREFIX_PATH=$(brew --prefix qt5)` #### Windows 1. Install Qt5, using their official installer. 1. If you plan to build with CMake, install CMake; if you're ok with qmake, you don't need to install anything on top of Qt. The commands in further sections imply that cmake/qmake is in your PATH - otherwise you have to prepend those commands with actual paths. As an option, it's a good idea to run a `qtenv2.bat` script that can be found in `C:\Qt\\\bin` (assuming you installed Qt to `C:\Qt`); the only thing it does is adding necessary paths to PATH. You might not want to run that script on system startup but it's very handy to setup the environment before building. For CMake, setting `CMAKE_PREFIX_PATH` in the same way as for macOS (see above), also helps. ### Using the library If you use CMake, `find_package(Quotient)` sets up the client code to use libQuotient, assuming the library development files are installed. There's no documented procedure to use a preinstalled library with qmake; consider introducing a submodule in your source tree and build it along with the rest of the application for now. Note also that qmake is considered for phase-out in Qt 6 so you should probably think of moving over to CMake eventually. Building with dynamic linkage are only tested on Linux at the moment and are a recommended way of linking your application with libQuotient on this platform. Feel free Static linkage is the default on Windows/macOS; feel free to experiment with dynamic linking and submit PRs if you get reusable results. The example/test application that comes with libQuotient, [qmc-example](https://github.com/quotient-im/libQuotient/tree/master/examples) includes most common use cases such as sending messages, uploading files, setting room state etc.; for more extensive usage check out the source code of [Quaternion](https://github.com/quotient-im/Quaternion) (the reference client of Quotient) or [Spectral](https://gitlab.com/b0/spectral). To ease the first step, `examples/CMakeLists.txt` is a good starting point for your own CMake-based project using libQuotient. ## Building the library [The source code is at GitHub](https://github.com/quotient-im/libQuotient). Checking out a certain commit or tag (rather than downloading the archive) along with submodules is strongly recommended. If you want to hack on the library as a part of another project (e.g. you are working on Quaternion but need to do some changes to the library code), it makes sense to make a recursive check out of that project (in this case, Quaternion) and update the library submodule (also recursively) to its master branch. Tags consisting of digits and periods represent released versions; tags ending with `-betaN` or `-rcN` mark pre-releases. If/when packaging pre-releases, it is advised to replace a dash with a tilde. ### CMake-based In the root directory of the project sources: ```shell script mkdir build_dir cd build_dir cmake .. # Pass -DCMAKE_PREFIX_PATH and -DCMAKE_INSTALL_PREFIX here if needed cmake --build . --target all ``` This will get you the compiled library in `build_dir` inside your project sources. Static builds are tested on all supported platforms. You can install the library with CMake: ```shell script cmake --build . --target install ``` This will also install cmake package config files; once this is done, you should be able to use `examples/CMakeLists.txt` to compile qmc-example with the _installed_ library. Installation of the `qmc-example` binary along with the rest of the library can be skipped by setting `QMATRIXCLIENT_INSTALL_EXAMPLE` to `OFF`. ### qmake-based The library provides a .pri file with an intention to be included from a bigger project's .pro file. As a starting point you can use `qmc-example.pro` that will build a minimal example of library usage for you. In the root directory of the project sources: ```shell script qmake qmc-example.pro make all ``` This will get you `debug/qmc-example` and `release/qmc-example` console executables that login to the Matrix server at matrix.org with credentials of your choosing (pass the username and password as arguments), run a sync long-polling loop and do some tests of the library API. Note that qmake didn't really know about C++17 until Qt 5.12 so if your Qt is older you may have quite a bit of warnings during the compilation process. Installing the standalone library with qmake is not implemented yet; PRs are welcome though. ## Troubleshooting #### Building fails If `cmake` fails with... ``` CMake Warning at CMakeLists.txt:11 (find_package): By not providing "FindQt5Widgets.cmake" in CMAKE_MODULE_PATH this project has asked CMake to find a package configuration file provided by "Qt5Widgets", but CMake did not find one. ``` ...then you need to set the right `-DCMAKE_PREFIX_PATH` variable, see above. #### Logging configuration libQuotient uses Qt's logging categories to make switching certain types of logging easier. In case of troubles at runtime (bugs, crashes) you can increase logging if you add the following to the `QT_LOGGING_RULES` environment variable: ``` quotient..= ``` where - `` is one of: `main`, `jobs`, `jobs.sync`, `events`, `events.state` (covering both the "usual" room state and account data), `events.messages`, `events.ephemeral`, `e2ee` and `profiler` (you can always find the full list in `lib/logging.cpp`) - `` is one of `debug`, `info`, and `warning` - `` is either `true` or `false`. `*` can be used as a wildcard for any part between two dots, and semicolon is used for a separator. Latter statements override former ones, so if you want to switch on all debug logs except `jobs` you can set ```shell script QT_LOGGING_RULES="quotient.*.debug=true;quotient.jobs.debug=false" ``` Note that `quotient` is a prefix that only works since version 0.6 of the library; 0.5.x and older used `libqmatrixclient` instead. If you happen to deal with both libQMatrixClient-era and Quotient-era versions, it's reasonable to use both prefixes, to make sure you're covered with no regard to the library version. For example, the above setting could look like ```shell script QT_LOGGING_RULES="libqmatrixclient.*.debug=true;libqmatrixclient.jobs.debug=false;quotient.*.debug=true;quotient.jobs.debug=false" ``` #### Cache format In case of troubles with room state and caching it may be useful to switch cache format from binary to JSON. To do that, set the following value in your client's configuration file/registry key (you might need to create the libqmatrixclient key for that): `libqmatrixclient/cache_type` to `json`. This will make cache saving and loading work slightly slower but the cache will be in a text JSON file (very long and unindented so prepare a good JSON viewer or text editor with JSON formatting capabilities). spectral/include/libQuotient/CONTRIBUTING.md0000644000175000000620000007147713566674122020557 0ustar dilingerstaff# Contributing Feedback and contributions are very welcome! Here's help on how to make contributions, divided into the following sections: The quick-read part: * general information, * vulnerability reporting, * documentation. The long-read part: * code changes, * how to check proposed changes before submitting them, * reuse of other libraries, frameworks etc. ## General information For specific proposals, please provide them as [pull requests](https://github.com/quotient-im/libQuotient/pulls) or [issues](https://github.com/quotient-im/libQuotient/issues) For general discussion, feel free to use our Matrix room: [#quotient:matrix.org](https://matrix.to/#/#quotient:matrix.org). If you're new to the project (or FLOSS in general), [issues tagged as easy](https://github.com/quotient-im/libQuotient/labels/easy) are smaller tasks that may typically take 1-3 days. You are welcome aboard! ### Pull requests and different branches recommended Pull requests are preferred, since they are specific. See the GitHub Help [articles about pull requests](https://help.github.com/articles/using-pull-requests/) to learn how to deal with them. We recommend creating different branches for different (logical) changes, and creating a pull request when you're done into the master branch. See the GitHub documentation on [creating branches](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/) and [using pull requests](https://help.github.com/articles/using-pull-requests/). ### How we handle proposals We use GitHub to track all changes via its [issue tracker](https://github.com/quotient-im/libQuotient/issues) and [pull requests](https://github.com/quotient-im/libQuotient/pulls). Specific changes are proposed using those mechanisms. Issues are assigned to an individual who works on it and then marks it complete. If there are questions or objections, the conversation area of that issue or pull request is used to resolve it. ### License Unless a contributor explicitly specifies otherwise, we assume contributors to agree that all contributed code is released either under *LGPL v2.1 or later*. This is more than just [LGPL v2.1 libQuotient now uses](./COPYING) because the project plans to switch to LGPL v3 for library code in the near future. Any components proposed for reuse should have a license that permits releasing a derivative work under *LGPL v2.1 or later* or LGPL v3. Moreover, the license of a proposed component should be approved by OSI, no exceptions. ## Vulnerability reporting (security issues) - see [SECURITY.md](./SECURITY.md) ## Documentation changes Most of the documentation is in Markdown format. All Markdown files use the .md filename extension. Any help on fixing/extending these is more than welcome. Where reasonable, limit yourself to Markdown that will be accepted by different markdown processors (e.g., what is specified by CommonMark or the original Markdown). In practice, as long as libQuotient is hosted at GitHub, [GFM (GitHub-flavoured Markdown)](https://help.github.com/articles/github-flavored-markdown/) is used to show those files in a browser, so it's fine to use its extensions. In particular, you can mark code snippets with the programming language used; blank lines separate paragraphs, newlines inside a paragraph do *not* force a line break. Beware - this is *not* the same markdown algorithm used by GitHub when it renders issue or pull comments; in those cases [newlines in paragraph-like content are considered as real line breaks](https://help.github.com/articles/writing-on-github/); unfortunately this other algorithm is *also* called GitHub-flavoured markdown. (Yes, it'd be better if there were different names for different things.) In your markdown, please don't use tab characters and avoid "bare" URLs. In a hyperlink, the link text and URL should be on the same line. While historically we didn't care about the line length in markdown texts (and more often than not put the whole paragraph into one line), this is subject to change anytime soon, with 80-character limit _recommendation_ (which is softer than the limit for C/C++ code) imposed on everything _except hyperlinks_ (because wrapping hyperlinks breaks the rendering). Do not use trailing two spaces for line breaks, since these cannot be seen and may be silently removed by some tools. If, for whatever reason, a blank line is not an option, use <br /> (an HTML break). ## End of TL;DR If you don't plan/have substantial contributions, you can end reading here. Further sections are for those who's going to actively hack on the library code. ## Code changes The code should strive to be DRY (don't repeat yourself), clear, and obviously correct (i.e. buildable). Some technical debt is inevitable, just don't bankrupt us with it. Refactoring is welcome. ### Generated C++ code for CS API The code in lib/csapi, lib/identity and lib/application-service, although it resides in Git, is actually generated from (a soft fork of) the official Matrix Swagger/OpenAPI definition files. Do not edit C++ files in these directories by hand! Now, if you're unhappy with something in there and want to improve the code, you have to understand the way these files are produced and setup some additional tooling. The shortest possible procedure resembling the below text can be found in .travis.yml (our Travis CI configuration actually regenerates those files upon every build). As described below, there is a handy build target for CMake; patches with a similar target for qmake are (you guessed it) very welcome. #### Why generate the code at all? Because before both original authors of libQuotient had to do monkey business of writing boilerplate code, with the same patterns, types etc., literally, for every single API endpoint, and one of the authors got fed up with it at some point in time. By then about 15 job classes were written; the entire API counts about 100 endpoints. Besides, the existing jobs had to be updated according to changes in CS API that have been, and will keep, coming. Other considerations can be found in [this talk about API description languages that briefly touches on GTAD](https://youtu.be/W5TmRozH-rg). #### Prerequisites for CS API code generation 1. Get the source code of GTAD and its dependencies, e.g. using the command: `git clone --recursive https://github.com/KitsuneRal/gtad.git` 2. Build GTAD: in the source code directory, do `cmake . && cmake --build .` (you might need to pass `-DCMAKE_PREFIX_PATH=`, similar to libQuotient itself). 3. Get the Matrix CS API definitions that are included in the matrix-doc repo: `git clone https://github.com/quotient-im/matrix-doc.git` (quotient-im/matrix-doc is a fork that's known to produce working code; you may want to use your own fork if you wish to alter something in the API). 4. If you plan to submit a PR or just would like the generated code to be formatted, you should either ensure you have clang-format (version 6 at least) in your PATH or pass the _absolute_ path to it by adding `-DCLANG_FORMAT=` to the CMake invocation below. #### Generating CS API contents 1. Pass additional configuration to CMake when configuring libQuotient: `-DMATRIX_DOC_PATH= -DGTAD_PATH=`. If everything's right, these two CMake variables will be mentioned in CMake output and will trigger configuration of an additional build target, see the next step. 2. Generate the code: `cmake --build --target update-api`; if you use CMake with GNU Make, you can just do `make update-api` instead. Building this target will create (overwriting without warning) `.h` and `.cpp` files in `lib/csapi`, `lib/identity`, `lib/application-service` for all YAML files it can find in `matrix-doc/api/client-server` and other files in `matrix-doc/api` these depend on. 3. Re-run CMake so that the build system knows about new files, if there are any. #### Changing generated code See the more detailed description of what GTAD is and how it works in the documentation on GTAD in its source repo. Only parts specific for libQuotient are described here. GTAD uses the following three kinds of sources: 1. OpenAPI files. Each file is treated as a separate source (because this is how GTAD works now). 2. A configuration file, in our case it's lib/csapi/gtad.yaml - this one is common for the whole API. 3. Source code template files: lib/csapi/{{base}}.*.mustache - also common. The mustache files have a templated (not in C++ sense) definition of a network job, deriving from BaseJob; each job class is prepended, if necessary, with data structure definitions used by this job. The look of those files is hideous for a newcomer; and the only known highlighter that can handle the combination of Mustache (originally a web templating language) and C++ is provided in CLion. To slightly simplify things some more or less generic constructs are defined in gtad.yaml (see its "mustache:" section). Adventurous souls that would like to figure what's going on in these files should speak up in the Quotient room - I (Kitsune) will be very glad to help you out. The types map in `gtad.yaml` is the central switchboard when it comes to matching OpenAPI types with C++ (and Qt) ones. It uses the following type attributes aside from pretty obvious "imports:": * `avoidCopy` - this attribute defines whether a const ref should be used instead of a value. For basic types like int this is obviously unnecessary; but compound types like `QVector` should rather be taken by reference when possible. * `moveOnly` - some types are not copyable at all and must be moved instead (an obvious example is anything "tainted" with a member of type `std::unique_ptr<>`). The template will use `T&&` instead of `T` or `const T&` to pass such types around. * `useOmittable` - wrap types that have no value with "null" semantics (i.e. number types and custom-defined data structures) into a special `Omittable<>` template defined in `converters.h` - a substitute for `std::optional` from C++17 (we're still at C++14 yet). * `omittedValue` - an alternative for `useOmittable`, just provide a value used for an omitted parameter. This is used for bool parameters which normally are considered false if omitted (or they have an explicit default value, passed in the "official" GTAD's `defaultValue` variable). * `initializer` - this is a _partial_ (see GTAD and Mustache documentation for explanations but basically it's a variable that is a Mustache template itself) that specifies how exactly a default value should be passed to the parameter. E.g., the default value for a `QString` parameter is enclosed into `QStringLiteral`. Instead of relying on the event structure definition in the OpenAPI files, `gtad.yaml` uses pointers to libQuotient's event structures: `EventPtr`, `RoomEventPtr` and `StateEventPtr`. Respectively, arrays of events, when encountered in OpenAPI definitions, are converted to `Events`, `RoomEvents` and `StateEvents` containers. When there's no way to figure the type from the definition, an opaque `QJsonObject` is used, leaving the conversion to the library and/or client code. ### Comments Whenever you add a new call to the library API that you expect to be used from client code, you must supply a proper doc-comment along with the call. Doxygen style is preferred; but JavaDoc is acceptable too. Some parts are not documented at all; adding doc-comments to them is highly encouraged. Doc-comments for summaries should be separate from those details. Either of the two following ways is fine, with considerable preference on the first one: 1. Use `///` for the summary comment and `/*! ... */` for details. 2. Use `\brief` (or `@brief`) for the summary, and follow with details after an empty doc-comment line. You can use either of the delimiters in that case. In the code, the advice for commenting is as follows: * Don't restate what's happening in the code unless it's not really obvious. We assume the readers to have at least some command of C++ and Qt. If your code is not obvious, consider rewriting it for clarity. * Both C++ and Qt still come with their arcane features and dark corners, and we don't want to limit anybody who'd feels they have a case for variable templates, raw literals, or use `qAsConst` to avoid container detachment. Use your experience to figure what might be less well-known to readers and comment such cases (references to web pages, Quotient wiki etc. are very much ok). * Make sure to document not so much "what" but more "why" certain code is done the way it is. In the worst case, the logic of the code can be reverse-engineered; you rarely can reverse-engineer the line of reasoning and the pitfalls avoided. ### API conventions Calls, data structures and other symbols not intended for use by clients should _not_ be exposed in (public) .h files, unless they are necessary to declare other public symbols. In particular, this involves private members (functions, typedefs, or variables) in public classes; use pimpl idiom to hide implementation details as much as possible. `_impl` namespace is reserved for definitions that should not be used by clients and are not covered by API guarantees. Note: As of now, all header files of libQuotient are considered public; this may change eventually. ### Code formatting The code style is defined by `.clang-format`, and in general, all C++ files should follow it. Files with minor deviations from the defined style are still accepted in PRs; however, unless explicitly marked with `// clang-format off` and `// clang-format on`, these deviations will be rectified any commit soon after. Additional considerations: * 4-space indents, no tabs, no trailing spaces, no last empty lines. If you spot the code abusing these - thank you for fixing it. * Prefer keeping lines within 80 characters. Slight overflows are ok only if that helps readability. * Please don't make "hypocritical structs" with protected or private members. In general, `struct` is used to denote a plain-old-data structure, rather than data+behaviour. If you need access control or are adding yet another non-trivial (construction, assignment) member function to a `struct`, just make it a `class` instead. * For newly created classes, keep to [the rule of 3/5/0](http://en.cppreference.com/w/cpp/language/rule_of_three) - make sure to read about the rule of zero if you haven't before, it's not what you might think it is. * Qt containers are generally preferred to STL containers; however, there are notable exceptions, and libQuotient already uses them: * `std::array` and `std::deque` have no direct counterparts in Qt. * Because of COW semantics, Qt containers cannot hold uncopyable classes. Classes without a default constructor are a problem too. Examples of that are `SyncRoomData` and `EventsArray<>`. Use STL containers for those but see the next point and also consider if you can supply a reasonable copy/default constructor. * STL containers can be freely used in code internal to a translation unit (i.e., in a certain .cpp file) _as long as that is not exposed in the API_. It's ok to use, e.g., `std::vector` instead of `QVector` to tighten up code where you don't need COW, or when dealing with uncopyable data structures (see the previous point). However, exposing STL containers in the API is not encouraged (except where absolutely necessary, e.g. we use `std::deque` for a timeline). Exposing STL containers or iterators in API intended for usage by QML code (e.g. in `Q_PROPERTY`) is unlikely to work and therefore unlikely to be accepted into `master`. * Although `std::unique_ptr<>` gives slightly stronger guarantees, `QScopedPointer<>` is better supported by Qt Creator's debugger UI and therefore is preferred. * Use `QVector` instead of `QList` where possible - see the [great article by Marc Mutz on Qt containers](https://marcmutz.wordpress.com/effective-qt/containers/) for details. ### Automated tests There's no testing framework as of now; either Catch or Qt Test or both will be used eventually. As a stopgap measure, qmc-example is used for automated functional testing. Therefore, any significant addition to the library API should be accompanied by a respective test in qmc-example. To add a test you should: - Add a new private slot to the `QMCTest` class. - Add to the beginning of the slot the line `running.push_back("Test name");`. - Add test logic to the slot, using `QMC_CHECK` macro to assert the test outcome. ALL (even failing) branches should conclude with a QMC_CHECK invocation, unless you intend to have a "DID NOT FINISH" message in the logs under certain conditions. - Call the slot from `QMCTest::startTests()`. `QMCTest` sets up some basic test fixture to help you with testing; notably by the moment `startTests()` is invoked you can rely on having a working connection in `c` member variable and a test room in `targetRoom` member variable. PRs to introduce a proper testing framework are very welcome (make sure to migrate tests from qmc-example though); shifting qmc-example to use Qt Test seems to be a particularly low-hanging fruit. ### Security, privacy, and performance Pay attention to security, and work *with* (not against) the usual security hardening mechanisms (however few in C++). `char *` and similar unchecked C-style read/write arrays are forbidden - use Qt containers or at the very least `std::array<>` instead. Where you see fit (usually with data structures), try to use smart pointers, especially `std::unique_ptr<>` or `QScopedPointer` instead of bare pointers. When dealing with `QObject`s, use the parent-child ownership semantics exercised by Qt (this is preferred to using smart pointers). Shared pointers are not used in the code so far; but if you find a particular use case where the strict semantic of unique pointers doesn't help and a shared pointer is necessary, feel free to step up with the working code and it will be considered for inclusion. Exercise the [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege) where reasonable and appropriate. Prefer less-coupled cohesive code. Protect private information, in particular passwords and email addresses. Absolutely _don't_ spill around this information in logs - use `access_token` and similar opaque ids instead, and only display those in UI where needed. Do not forget about local access to data (in particular, be very careful when storing something in temporary files, let alone permanent configuration or state). Avoid mechanisms that could be used for tracking where possible (we do need to verify people are logged in but that's pretty much it), and ensure that third parties can't use interactions for tracking. Matrix protocols evolve towards decoupling the personally identifiable information from user activity entirely - follow this trend. We want the software to have decent performance for typical users. At the same time we keep libQuotient single-threaded as much as possible, to keep the code simple. That means being cautious about operation complexity (read about big-O notation if you need a kickstart on the topic). This especially refers to operations on the whole timeline and the list of users - each of these can have tens of thousands of elements so even operations with linear complexity, if heavy enough, can produce noticeable GUI freezing. When you don't see a way to reduce algorithmic complexity, embed occasional `processEvents()` invocations in heavy loops (see `Connection::saveState()` to get the idea). Having said that, there's always a trade-off between various attributes; in particular, readability and maintainability of the code is more important than squeezing every bit out of that clumsy algorithm. Beware of premature optimization and have profiling data around before going into some hardcore optimization. Speaking of profiling logs (see README.md on how to turn them on) - in order to reduce small timespan logging spam, there's a default limit of at least 200 microseconds to log most operations with the PROFILER (aka quotient.profile.debug) logging category. You can override this limit by passing the new value (in microseconds) in PROFILER_LOG_USECS to the compiler. In the future, this parameter will be made changeable at runtime _if_ needed. ## How to check proposed changes before submitting them Checking the code on at least one configuration is essential; if you only have a hasty fix that doesn't even compile, better make an issue and put a link to your commit into it (with an explanation what it is about and why). ### Standard checks The following warnings configuration is applied with GCC and Clang when using CMake: `-W -Wall -Wextra -pedantic -Werror=return-type -Wno-unused-parameter -Wno-gnu-zero-variadic-macro-arguments` (the last one is to mute a warning triggered by Qt code for debug logging). We don't turn most of the warnings to errors but please treat them as such. In Qt Creator, the following line can be used with the Clang code model (before Qt Creator 4.7 you should explicitly enable the Clang code model plugin): `-Weverything -Werror=return-type -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-unused-macros -Wno-newline-eof -Wno-exit-time-destructors -Wno-global-constructors -Wno-gnu-zero-variadic-macro-arguments -Wno-documentation -Wno-missing-prototypes -Wno-shadow-field-in-constructor -Wno-padded -Wno-weak-vtables -Wno-unknown-attributes -Wno-comma`. ### Continuous Integration We use Travis CI to check buildability and smoke-testing on Linux (GCC, Clang) and MacOS (Clang), and AppVeyor CI to build on Windows (MSVC). Every PR will go through these, and you'll see the traffic lights from them on the PR page. If your PR fails on any platform double-check that it's not your code causing it - and fix it if it is. ### clang-format We strongly recommend using clang-format or, even better, use an IDE that supports it. This will lay a tedious task of following the assumed code style from your shoulders over to your computer. ### Other tools Recent versions of Qt Creator and CLion can automatically run your code through clang-tidy. The following list of clang-tidy checks slows Qt Creator analysis quite considerably but gives a good insight without too many false positives: `-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-inaccurate-erase,bugprone-integer-division,bugprone-move-forwarding-reference,bugprone-string-constructor,bugprone-undefined-memory-manipulation,bugprone-use-after-move,bugprone-virtual-near-miss,cert-dcl03-c,cert-dcl21-cpp,cert-dcl50-cpp,cert-dcl54-cpp,cert-dcl58-cpp,cert-env33-c,cert-err09-cpp,cert-err34-c,cert-err52-cpp,cert-err60-cpp,cert-err61-cpp,cert-fio38-c,cert-flp30-c,cert-msc30-c,cert-msc50-cpp,cert-oop11-cpp,cppcoreguidelines-c-copy-assignment-signature,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-slicing,hicpp-deprecated-headers,hicpp-invalid-access-moved,hicpp-member-init,hicpp-move-const-arg,hicpp-named-parameter,hicpp-new-delete-operators,hicpp-static-assert,hicpp-undelegated-constructor,hicpp-use-*,misc-misplaced-const,misc-new-delete-overloads,misc-non-copyable-objects,misc-redundant-expression,misc-static-assert,misc-throw-by-value-catch-by-reference,misc-unconventional-assign-operator,misc-uniqueptr-reset-release,misc-unused-*,modernize-loop-convert,modernize-pass-by-value,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-*,performance-faster-string-find,performance-for-range-copy,performance-implicit-conversion-in-loop,performance-inefficient-*,performance-move-*,performance-type-promotion-in-math-fn,performance-unnecessary-*,readability-delete-null-pointer,readability-else-after-return,readability-inconsistent-declaration-parameter-name,readability-misleading-indentation,readability-redundant-*,readability-simplify-boolean-expr,readability-static-definition-in-anonymous-namespace,readability-uniqueptr-delete-release`. Qt Creator, in addition, knows about clazy, an even deeper Qt-aware static analysis tool. Even level 1 clazy eats away CPU but produces some very relevant notices that are easy to overlook otherwise, such as possible unintended copying of a Qt container, or unguarded null pointers. You can use this time to time (see Analyze menu in Qt Creator) instead of hogging your machine with deep analysis as you type. ## Git commit messages When writing git commit messages, try to follow the guidelines in [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/): 1. Separate subject from body with a blank line 2. Be reasonable on the subject line length, because this is what we see in commit logs. Try to fit in 50 characters whenever possible. 3. Capitalize the subject line 4. Do not end the subject line with a period 5. Use the imperative mood in the subject line (*command* form) (we don't always practice this ourselves but let's try). 6. Use the body to explain what and why vs. how (git tracks how it was changed in detail, don't repeat that). Sometimes a quick overview of "how" is acceptable if a commit is huge - but maybe split a commit into smaller ones, to begin with? ## Reuse (libraries, frameworks, etc.) C++ is unfortunately not very coherent about SDK/package management, and we try to keep building the library as easy as possible. Because of that we are very conservative about adding dependencies to libQuotient. That relates to additional Qt components and even more to other libraries. Fortunately, even the Qt components now in use (Qt Core and Network) are very feature-rich and provide plenty of ready-made stuff. Regardless of the above paragraph (and as mentioned earlier in the text), we're now looking at possible options for futures and automated testing, so PRs onboarding those will be considered with much gratitude. Some cases need additional explanation: * Before rolling out your own super-optimised container or algorithm written from scratch, take a good long look through documentation on Qt and C++ standard library. Please try to reuse the existing facilities as much as possible. * You should have a good reason (or better several ones) to add a component from KDE Frameworks. We don't rule this out and there's no prejudice against KDE; it just so happened that KDE Frameworks is one of most obvious reuse candidates but so far none of these components survived as libQuotient deps. So we are cautious. Extra notice to KDE folks: I'll be happy if an addon library on top of libQuotient is made using KDE facilities, and I'm willing to take part in its evolution; but please also respect LXDE people who normally don't have KDE frameworks installed. * Never forget that libQuotient is aimed to be a non-visual library; QtGui in dependencies is only driven by (entirely offscreen) dealing with QImages. While there's a bunch of visual code (in C++ and QML) shared between Quotient-enabled _applications_, this is likely to end up in a separate (Quotient-enabled) library, rather than libQuotient itself. ## Attribution This text is based on CONTRIBUTING.md from CII Best Practices Badge project, which is a collective work of its contributors (many thanks!). The text itself is licensed under CC-BY-4.0. spectral/include/libQuotient/.gitignore0000644000175000000620000000016613566674122020301 0ustar dilingerstaffbuild .kdev4 # Qt Creator project file *.user* # qmake derivatives Makefile* object_script.* .qmake* debug/ release/spectral/include/libQuotient/qmc-example.pro0000644000175000000620000000031713566674122021242 0ustar dilingerstaffTEMPLATE = app CONFIG *= c++1z warn_on object_parallel_to_source windows { CONFIG *= console } include(libquotient.pri) SOURCES += examples/qmc-example.cpp DISTFILES += \ .valgrind.qmc-example.supp spectral/include/libQuotient/ISSUE_TEMPLATE.md0000644000175000000620000000313413566674122021014 0ustar dilingerstaff ### Description Describe here the problem that you are experiencing, or the feature you are requesting. ### Steps to reproduce - For bugs, list the steps - that reproduce the bug - using hyphens as bullet points Describe how what happens differs from what you expected. libQuotient-based clients either have a log file or dump log to the standard output. If you can identify any log snippets relevant to your issue, please include those here (please be careful to remove any personal or private data): ### Version information - **The client application**: - **libQuotient version if you know it**: - **Qt version**: - **Install method**: - **Platform**: spectral/include/libQuotient/.appveyor.yml0000644000175000000620000000315713566674122020762 0ustar dilingerstaffimage: Visual Studio 2017 environment: #DEPLOY_DIR: quotient-%APPVEYOR_BUILD_VERSION% matrix: - QTDIR: C:\Qt\5.13\msvc2017_64 VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat" PLATFORM: - QTDIR: C:\Qt\5.13\msvc2017 VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars32.bat" PLATFORM: x86 init: - call "%QTDIR%\bin\qtenv2.bat" - set PATH=C:\Qt\Tools\QtCreator\bin;%PATH% - call "%VCVARS%" %PLATFORM% - cd /D "%APPVEYOR_BUILD_FOLDER%" before_build: - git submodule update --init --recursive - git clone https://gitlab.matrix.org/matrix-org/olm.git - cd olm - cmake -G "NMake Makefiles JOM" -H. -Bbuild -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=install - cmake --build build - cmake --build build --target install - cd .. - cmake -G "NMake Makefiles JOM" -H. -Bbuild -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX="%DEPLOY_DIR%" -DOlm_DIR="olm/install/lib/cmake/Olm" build_script: - cmake --build build # qmake uses olm just built by CMake - it can't build olm on its own. - qmake "INCLUDEPATH += olm/install/include" "LIBS += -Lbuild" "LIBS += -Lolm/install/lib" && jom #after_build: #- cmake --build build --target install #- 7z a quotient.zip "%DEPLOY_DIR%\" # Uncomment this to connect to the AppVeyor build worker #on_finish: # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) test: off #artifacts: #- path: quotient.zip spectral/include/libQuotient/.git0000644000175000000620000000005713566674121017073 0ustar dilingerstaffgitdir: ../../.git/modules/include/libQuotient spectral/include/libQuotient/Quotient.pc.in0000644000175000000620000000045613566674122021054 0ustar dilingerstaffprefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@ Name: Quotient Description: A Qt5 library to write cross-platfrom clients for Matrix Version: @API_VERSION@ Cflags: -I${includedir} Libs: -L${libdir} -lQuotient spectral/include/libQuotient/CMakeLists.txt0000644000175000000620000002515513566674122021056 0ustar dilingerstaffcmake_minimum_required(VERSION 3.10) if (POLICY CMP0092) cmake_policy(SET CMP0092 NEW) endif() set(API_VERSION "0.6") project(Quotient VERSION "${API_VERSION}.0" LANGUAGES CXX) option(QUOTIENT_INSTALL_EXAMPLE "install qmc-example application" ON) include(CheckCXXCompilerFlag) if (NOT WIN32) include(GNUInstallDirs) endif(NOT WIN32) # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) # Set a default build type if none was specified if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to 'Debug' as none was specified") set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build" FORCE) # Set the possible values of build type for cmake-gui set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() if (NOT CMAKE_INSTALL_LIBDIR) set(CMAKE_INSTALL_LIBDIR ".") endif() if (NOT CMAKE_INSTALL_BINDIR) set(CMAKE_INSTALL_BINDIR ".") endif() if (NOT CMAKE_INSTALL_INCLUDEDIR) set(CMAKE_INSTALL_INCLUDEDIR "include") endif() if (MSVC) add_compile_options(/EHsc /W4 /wd4100 /wd4127 /wd4242 /wd4244 /wd4245 /wd4267 /wd4365 /wd4456 /wd4459 /wd4464 /wd4505 /wd4514 /wd4571 /wd4619 /wd4623 /wd4625 /wd4626 /wd4706 /wd4710 /wd4774 /wd4820 /wd4946 /wd5026 /wd5027) else() foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu-zero-variadic-macro-arguments) CHECK_CXX_COMPILER_FLAG("-W${FLAG}" WARN_${FLAG}_SUPPORTED) if ( WARN_${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "(^| )-W?${FLAG}($| )") add_compile_options(-W${FLAG}) endif () endforeach () endif() find_package(Qt5 5.9 REQUIRED Network Gui Multimedia) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) if ((NOT DEFINED USE_INTREE_LIBQOLM OR USE_INTREE_LIBQOLM) AND EXISTS ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm/lib/utils.h) add_subdirectory(3rdparty/libQtOlm EXCLUDE_FROM_ALL) include_directories(3rdparty/libQtOlm) if (NOT DEFINED USE_INTREE_LIBQOLM) set (USE_INTREE_LIBQOLM 1) endif () endif () if (NOT USE_INTREE_LIBQOLM) find_package(QtOlm 0.1.0 REQUIRED) if (NOT QtOlm_FOUND) message( WARNING "libQtOlm not found; configuration will most likely fail.") message( WARNING "Make sure you have installed libQtOlm development files") message( WARNING "as a package or checked out the library sources in lib/.") message( WARNING "See also BUILDING.md") endif () endif () if (GTAD_PATH) get_filename_component(ABS_GTAD_PATH "${GTAD_PATH}" REALPATH) endif () if (MATRIX_DOC_PATH) get_filename_component(ABS_API_DEF_PATH "${MATRIX_DOC_PATH}/api" REALPATH) endif () if (ABS_GTAD_PATH AND ABS_API_DEF_PATH) if (NOT CLANG_FORMAT) set(CLANG_FORMAT clang-format) endif() get_filename_component(ABS_CLANG_FORMAT "${CLANG_FORMAT}" PROGRAM) endif() message( STATUS ) message( STATUS "=============================================================================" ) message( STATUS " ${PROJECT_NAME} Build Information" ) message( STATUS "=============================================================================" ) message( STATUS "Version: ${PROJECT_VERSION}, API version: ${API_VERSION}") if (CMAKE_BUILD_TYPE) message( STATUS "Build type: ${CMAKE_BUILD_TYPE}") endif(CMAKE_BUILD_TYPE) message( STATUS "Using compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) message( STATUS "Install Prefix: ${CMAKE_INSTALL_PREFIX}" ) message( STATUS "Using Qt ${Qt5_VERSION} at ${Qt5_Prefix}" ) if (ABS_API_DEF_PATH AND ABS_GTAD_PATH) message( STATUS "Generating API stubs enabled (use --target update-api)" ) message( STATUS " Using GTAD at ${ABS_GTAD_PATH}" ) message( STATUS " Using API files at ${ABS_API_DEF_PATH}" ) if (ABS_CLANG_FORMAT) message( STATUS "clang-format is at ${ABS_CLANG_FORMAT}") else () message( STATUS "${CLANG_FORMAT} is NOT FOUND; API files won't be reformatted") endif () endif () find_package(Git) if (USE_INTREE_LIBQOLM) message( STATUS "Using in-tree libQtOlm") if (GIT_FOUND) execute_process(COMMAND "${GIT_EXECUTABLE}" rev-parse -q HEAD WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/3rdparty/libQtOlm OUTPUT_VARIABLE QTOLM_GIT_SHA1 OUTPUT_STRIP_TRAILING_WHITESPACE) message( STATUS " Library git SHA1: ${QTOLM_GIT_SHA1}") endif (GIT_FOUND) else () message( STATUS "Using libQtOlm ${QtOlm_VERSION} at ${QtOlm_DIR}") endif () message( STATUS "=============================================================================" ) message( STATUS ) # Set up source files set(lib_SRCS lib/networkaccessmanager.cpp lib/connectiondata.cpp lib/connection.cpp lib/logging.cpp lib/room.cpp lib/user.cpp lib/avatar.cpp lib/syncdata.cpp lib/settings.cpp lib/networksettings.cpp lib/converters.cpp lib/util.cpp lib/encryptionmanager.cpp lib/eventitem.cpp lib/events/event.cpp lib/events/roomevent.cpp lib/events/stateevent.cpp lib/events/eventcontent.cpp lib/events/roomcreateevent.cpp lib/events/roomtombstoneevent.cpp lib/events/roommessageevent.cpp lib/events/roommemberevent.cpp lib/events/typingevent.cpp lib/events/receiptevent.cpp lib/events/reactionevent.cpp lib/events/callanswerevent.cpp lib/events/callcandidatesevent.cpp lib/events/callhangupevent.cpp lib/events/callinviteevent.cpp lib/events/directchatevent.cpp lib/events/encryptionevent.cpp lib/events/encryptedevent.cpp lib/jobs/requestdata.cpp lib/jobs/basejob.cpp lib/jobs/syncjob.cpp lib/jobs/mediathumbnailjob.cpp lib/jobs/downloadfilejob.cpp ) set(CSAPI_DIR csapi) set(ASAPI_DEF_DIR application-service/definitions) set(ISAPI_DEF_DIR identity/definitions) foreach (D ${CSAPI_DIR} ${CSAPI_DIR}/definitions ${CSAPI_DIR}/definitions/wellknown ${ASAPI_DEF_DIR} ${ISAPI_DEF_DIR}) aux_source_directory(lib/${D} api_SRCS) endforeach() # Make no mistake: CMake cannot run gtad first and then populate the list of # resulting api_SRCS files. In other words, placing the above foreach after # the custom targets definition won't bring the desired result: # CMake will execute it at cmake invocation and gtad will only run later # when building the update-api target. If you see that gtad has created # new files you have to re-run cmake. # TODO: check `file(GLOB_RECURSE ... CONFIGURE_DEPENDS)` (from CMake 3.14) if (MATRIX_DOC_PATH AND GTAD_PATH) set(FULL_CSAPI_DIR lib/${CSAPI_DIR}) set(FULL_CSAPI_SRC_DIR ${ABS_API_DEF_PATH}/client-server) file(GLOB_RECURSE API_DEFS RELATIVE ${PROJECT_SOURCE_DIR} ${FULL_CSAPI_SRC_DIR}/*.yaml ${ABS_API_DEF_PATH}/${ASAPI_DEF_DIR}/*.yaml ${ABS_API_DEF_PATH}/${ISAPI_DEF_DIR}/*.yaml ) add_custom_target(update-api ${ABS_GTAD_PATH} --config ${CSAPI_DIR}/gtad.yaml --out ${CSAPI_DIR} ${FULL_CSAPI_SRC_DIR} old_sync.yaml- room_initial_sync.yaml- # deprecated sync.yaml- # we have a better handcrafted implementation WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/lib SOURCES ${FULL_CSAPI_DIR}/gtad.yaml ${FULL_CSAPI_DIR}/{{base}}.h.mustache ${FULL_CSAPI_DIR}/{{base}}.cpp.mustache ${API_DEFS} VERBATIM ) if (ABS_CLANG_FORMAT) # TODO: list(TRANSFORM) is available from CMake 3.12 foreach (S ${api_SRCS}) string (REGEX REPLACE ".cpp$" ".h" H ${S}) list(APPEND api_HDRS ${H}) endforeach() set(CLANG_FORMAT_ARGS -i -sort-includes ${CLANG_FORMAT_ARGS}) add_custom_command(TARGET update-api POST_BUILD COMMAND ${ABS_CLANG_FORMAT} ${CLANG_FORMAT_ARGS} ${api_SRCS} COMMAND ${ABS_CLANG_FORMAT} ${CLANG_FORMAT_ARGS} ${api_HDRS} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} VERBATIM COMMENT Formatting files ) endif() endif() set(example_SRCS examples/qmc-example.cpp) add_library(${PROJECT_NAME} ${lib_SRCS} ${api_SRCS}) set_target_properties(${PROJECT_NAME} PROPERTIES VERSION "${PROJECT_VERSION}" SOVERSION ${API_VERSION} INTERFACE_${PROJECT_NAME}_MAJOR_VERSION ${API_VERSION} ) set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY COMPATIBLE_INTERFACE_STRING ${PROJECT_NAME}_MAJOR_VERSION) target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) target_include_directories(${PROJECT_NAME} PUBLIC $ $ ) target_link_libraries(${PROJECT_NAME} QtOlm Qt5::Core Qt5::Network Qt5::Gui Qt5::Multimedia) add_executable(qmc-example ${example_SRCS}) target_link_libraries(qmc-example Qt5::Core Quotient) configure_file(Quotient.pc.in ${CMAKE_CURRENT_BINARY_DIR}/Quotient.pc @ONLY NEWLINE_STYLE UNIX) # Installation install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}Targets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) install(DIRECTORY lib/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING PATTERN "*.h") include(CMakePackageConfigHelpers) # NB: SameMajorVersion doesn't really work yet, as we're within 0.x trail. # Maybe consider jumping the gun and releasing 1.0, as semver advises? write_basic_package_version_file( "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/QuotientConfigVersion.cmake" COMPATIBILITY SameMajorVersion ) export(PACKAGE ${PROJECT_NAME}) export(EXPORT ${PROJECT_NAME}Targets FILE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/${PROJECT_NAME}Targets.cmake") configure_file(cmake/QuotientConfig.cmake "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/QuotientConfig.cmake" COPYONLY ) set(ConfigFilesLocation "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") install(EXPORT ${PROJECT_NAME}Targets FILE ${PROJECT_NAME}Targets.cmake DESTINATION ${ConfigFilesLocation}) install(FILES cmake/QuotientConfig.cmake "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}/QuotientConfigVersion.cmake" DESTINATION ${ConfigFilesLocation} ) install(EXPORT_ANDROID_MK QuotientTargets DESTINATION share/ndk-modules) if (WIN32) install(FILES mime/packages/freedesktop.org.xml DESTINATION mime/packages) endif (WIN32) if (QUOTIENT_INSTALL_EXAMPLE) install(TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) endif (QUOTIENT_INSTALL_EXAMPLE) if (UNIX AND NOT APPLE) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/Quotient.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) endif() spectral/include/libQuotient/examples/0002755000175000000620000000000013566674122020126 5ustar dilingerstaffspectral/include/libQuotient/examples/qmc-example.cpp0000644000175000000620000005556513566674122023061 0ustar dilingerstaff #include "connection.h" #include "room.h" #include "user.h" #include "csapi/joining.h" #include "csapi/leaving.h" #include "csapi/room_send.h" #include "events/reactionevent.h" #include "events/simplestateevents.h" #include #include #include #include #include #include #include using namespace Quotient; using std::cout; using std::endl; using namespace std::placeholders; class QMCTest : public QObject { public: QMCTest(Connection* conn, QString testRoomName, QString source); private slots: // clang-format off void setupAndRun(); void onNewRoom(Room* r); void run(); void doTests(); void loadMembers(); void sendMessage(); void sendReaction(const QString& targetEvtId); void sendFile(); void checkFileSendingOutcome(const QString& txnId, const QString& fileName); void setTopic(); void addAndRemoveTag(); void sendAndRedact(); bool checkRedactionOutcome(const QString& evtIdToRedact); void markDirectChat(); void checkDirectChatOutcome( const Connection::DirectChatsMap& added); void conclude(); void finalize(); // clang-format on private: QScopedPointer c; QStringList running; QStringList succeeded; QStringList failed; QString origin; QString targetRoomName; Room* targetRoom = nullptr; bool validatePendingEvent(const QString& txnId); }; #define QMC_CHECK(description, condition) \ { \ Q_ASSERT(running.removeOne(description)); \ if (!!(condition)) { \ succeeded.push_back(description); \ cout << (description) << " successful" << endl; \ if (targetRoom) \ targetRoom->postMessage(origin % ": " % (description) \ % " successful", \ MessageEventType::Notice); \ } else { \ failed.push_back(description); \ cout << (description) << " FAILED" << endl; \ if (targetRoom) \ targetRoom->postPlainText(origin % ": " % (description) \ % " FAILED"); \ } \ } bool QMCTest::validatePendingEvent(const QString& txnId) { auto it = targetRoom->findPendingEvent(txnId); return it != targetRoom->pendingEvents().end() && it->deliveryStatus() == EventStatus::Submitted && (*it)->transactionId() == txnId; } QMCTest::QMCTest(Connection* conn, QString testRoomName, QString source) : c(conn), origin(std::move(source)), targetRoomName(std::move(testRoomName)) { if (!origin.isEmpty()) cout << "Origin for the test message: " << origin.toStdString() << endl; if (!targetRoomName.isEmpty()) cout << "Test room name: " << targetRoomName.toStdString() << endl; connect(c.data(), &Connection::connected, this, &QMCTest::setupAndRun); connect(c.data(), &Connection::loadedRoomState, this, &QMCTest::onNewRoom); // Big countdown watchdog QTimer::singleShot(180000, this, &QMCTest::conclude); } void QMCTest::setupAndRun() { Q_ASSERT(!c->homeserver().isEmpty() && c->homeserver().isValid()); Q_ASSERT(c->domain() == c->userId().section(':', 1)); cout << "Connected, server: " << c->homeserver().toDisplayString().toStdString() << endl; cout << "Access token: " << c->accessToken().toStdString() << endl; if (!targetRoomName.isEmpty()) { cout << "Joining " << targetRoomName.toStdString() << endl; running.push_back("Join room"); auto joinJob = c->joinRoom(targetRoomName); connect(joinJob, &BaseJob::failure, this, [this] { QMC_CHECK("Join room", false); conclude(); }); // Connection::joinRoom() creates a Room object upon JoinRoomJob::success // but this object is empty until the first sync is done. connect(joinJob, &BaseJob::success, this, [this, joinJob] { targetRoom = c->room(joinJob->roomId(), JoinState::Join); QMC_CHECK("Join room", targetRoom != nullptr); run(); }); } else run(); } void QMCTest::onNewRoom(Room* r) { cout << "New room: " << r->id().toStdString() << endl << " Name: " << r->name().toStdString() << endl << " Canonical alias: " << r->canonicalAlias().toStdString() << endl << endl; connect(r, &Room::aboutToAddNewMessages, r, [r](RoomEventsRange timeline) { cout << timeline.size() << " new event(s) in room " << r->canonicalAlias().toStdString() << endl; // for (const auto& item: timeline) // { // cout << "From: " // << r->roomMembername(item->senderId()).toStdString() // << endl << "Timestamp:" // << item->timestamp().toString().toStdString() << endl // << "JSON:" << endl << // item->originalJson().toStdString() << endl; // } }); } void QMCTest::run() { c->setLazyLoading(true); c->syncLoop(); connectSingleShot(c.data(), &Connection::syncDone, this, &QMCTest::doTests); connect(c.data(), &Connection::syncDone, c.data(), [this] { cout << "Sync complete, " << running.size() << " test(s) in the air: " << running.join(", ").toStdString() << endl; if (running.isEmpty()) conclude(); }); } void QMCTest::doTests() { cout << "Starting tests" << endl; loadMembers(); // Add here tests not requiring the test room if (targetRoomName.isEmpty()) return; sendMessage(); sendFile(); setTopic(); addAndRemoveTag(); sendAndRedact(); markDirectChat(); // Add here tests with the test room } void QMCTest::loadMembers() { running.push_back("Loading members"); auto* r = c->roomByAlias(QStringLiteral("#quotient:matrix.org"), JoinState::Join); if (!r) { cout << "#test:matrix.org is not found in the test user's rooms" << endl; QMC_CHECK("Loading members", false); return; } // It's not exactly correct because an arbitrary server might not support // lazy loading; but in the absence of capabilities framework we assume // it does. if (r->memberNames().size() >= r->joinedCount()) { cout << "Lazy loading doesn't seem to be enabled" << endl; QMC_CHECK("Loading members", false); return; } r->setDisplayed(); connect(r, &Room::allMembersLoaded, [this, r] { QMC_CHECK("Loading members", r->memberNames().size() >= r->joinedCount()); }); } void QMCTest::sendMessage() { running.push_back("Message sending"); cout << "Sending a message" << endl; auto txnId = targetRoom->postPlainText("Hello, " % origin % " is here"); if (!validatePendingEvent(txnId)) { cout << "Invalid pending event right after submitting" << endl; QMC_CHECK("Message sending", false); return; } connectUntil( targetRoom, &Room::pendingEventAboutToMerge, this, [this, txnId](const RoomEvent* evt, int pendingIdx) { const auto& pendingEvents = targetRoom->pendingEvents(); Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); if (evt->transactionId() != txnId) return false; QMC_CHECK("Message sending", is(*evt) && !evt->id().isEmpty() && pendingEvents[size_t(pendingIdx)]->transactionId() == evt->transactionId()); sendReaction(evt->id()); return true; }); } void QMCTest::sendReaction(const QString& targetEvtId) { running.push_back("Reaction sending"); cout << "Reacting to the newest message in the room" << endl; Q_ASSERT(targetRoom->timelineSize() > 0); const auto key = QStringLiteral("+1"); auto txnId = targetRoom->postReaction(targetEvtId, key); if (!validatePendingEvent(txnId)) { cout << "Invalid pending event right after submitting" << endl; QMC_CHECK("Reaction sending", false); return; } // TODO: Check that it came back as a reaction event and that it attached to // the right event connectUntil( targetRoom, &Room::updatedEvent, this, [this, txnId, key, targetEvtId](const QString& actualTargetEvtId) { if (actualTargetEvtId != targetEvtId) return false; const auto reactions = targetRoom->relatedEvents( targetEvtId, EventRelation::Annotation()); // It's a test room, assuming no interference there should // be exactly one reaction if (reactions.size() != 1) { QMC_CHECK("Reaction sending", false); } else { const auto* evt = eventCast(reactions.back()); QMC_CHECK("Reaction sending", is(*evt) && !evt->id().isEmpty() && evt->relation().key == key && evt->transactionId() == txnId); } return true; }); } void QMCTest::sendFile() { running.push_back("File sending"); cout << "Sending a file" << endl; auto* tf = new QTemporaryFile; if (!tf->open()) { cout << "Failed to create a temporary file" << endl; QMC_CHECK("File sending", false); return; } tf->write("Test"); tf->close(); // QFileInfo::fileName brings only the file name; QFile::fileName brings // the full path const auto tfName = QFileInfo(*tf).fileName(); cout << "Sending file" << tfName.toStdString() << endl; const auto txnId = targetRoom->postFile("Test file", QUrl::fromLocalFile(tf->fileName())); if (!validatePendingEvent(txnId)) { cout << "Invalid pending event right after submitting" << endl; QMC_CHECK("File sending", false); delete tf; return; } // FIXME: Clean away connections (connectUntil doesn't help here). connect(targetRoom, &Room::fileTransferCompleted, this, [this, txnId, tf, tfName](const QString& id) { auto fti = targetRoom->fileTransferInfo(id); Q_ASSERT(fti.status == FileTransferInfo::Completed); if (id != txnId) return; delete tf; checkFileSendingOutcome(txnId, tfName); }); connect(targetRoom, &Room::fileTransferFailed, this, [this, txnId, tf](const QString& id, const QString& error) { if (id != txnId) return; targetRoom->postPlainText(origin % ": File upload failed: " % error); delete tf; QMC_CHECK("File sending", false); }); } void QMCTest::checkFileSendingOutcome(const QString& txnId, const QString& fileName) { auto it = targetRoom->findPendingEvent(txnId); if (it == targetRoom->pendingEvents().end()) { cout << "Pending file event dropped before upload completion" << endl; QMC_CHECK("File sending", false); return; } if (it->deliveryStatus() != EventStatus::FileUploaded) { cout << "Pending file event status upon upload completion is " << it->deliveryStatus() << " != FileUploaded(" << EventStatus::FileUploaded << ')' << endl; QMC_CHECK("File sending", false); return; } connectUntil( targetRoom, &Room::pendingEventAboutToMerge, this, [this, txnId, fileName](const RoomEvent* evt, int pendingIdx) { const auto& pendingEvents = targetRoom->pendingEvents(); Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); if (evt->transactionId() != txnId) return false; cout << "File event " << txnId.toStdString() << " arrived in the timeline" << endl; visit( *evt, [&](const RoomMessageEvent& e) { QMC_CHECK( "File sending", !e.id().isEmpty() && pendingEvents[size_t(pendingIdx)]->transactionId() == txnId && e.hasFileContent() && e.content()->fileInfo()->originalName == fileName); }, [this](const RoomEvent&) { QMC_CHECK("File sending", false); }); return true; }); } void QMCTest::setTopic() { static const char* const stateTestName = "State setting test"; running.push_back(stateTestName); const auto newTopic = c->generateTxnId(); // Just a way to get a unique id targetRoom->setTopic(newTopic); // Sets the state by proper means const auto fakeTopic = c->generateTxnId(); const auto fakeTxnId = targetRoom->postJson(RoomTopicEvent::matrixTypeId(), // Fake state event RoomTopicEvent(fakeTopic).contentJson()); connectUntil(targetRoom, &Room::topicChanged, this, [this, newTopic] { if (targetRoom->topic() == newTopic) { QMC_CHECK(stateTestName, true); return true; } return false; }); // Older Synapses allowed sending fake state events through, although // did not process them; // https://github.com/matrix-org/synapse/pull/5805 // changed that and now Synapse 400's in response to fake state events. // The following two-step approach handles both cases, assuming that // Room::pendingEventChanged() with EventStatus::ReachedServer is guaranteed // to be emitted before Room::pendingEventAboutToMerge. connectUntil( targetRoom, &Room::pendingEventChanged, this, [this, fakeTopic, fakeTxnId](int pendingIdx) { const auto& pendingEvents = targetRoom->pendingEvents(); Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size())); const auto& evt = pendingEvents[pendingIdx]; if (evt->transactionId() != fakeTxnId) return false; // If Synapse rejected the event, skip the immunity test. if (evt.deliveryStatus() == EventStatus::SendingFailed) return true; if (evt.deliveryStatus() != EventStatus::ReachedServer) return false; // All before was just a preparation, this is where the test starts. static const char* const fakeStateTestName = "Fake state event immunity test"; running.push_back(fakeStateTestName); connectUntil( targetRoom, &Room::pendingEventAboutToMerge, this, [this, fakeTopic](const RoomEvent* e, int) { if (e->contentJson().value("topic").toString() != fakeTopic) return false; // Wait on for the right event QMC_CHECK(fakeStateTestName, !e->isStateEvent()); return true; }); return true; }); } void QMCTest::addAndRemoveTag() { running.push_back("Tagging test"); static const auto TestTag = QStringLiteral("org.quotient.test"); // Pre-requisite if (targetRoom->tags().contains(TestTag)) targetRoom->removeTag(TestTag); // Connect first because the signal is emitted synchronously. connect(targetRoom, &Room::tagsChanged, targetRoom, [=] { cout << "Room " << targetRoom->id().toStdString() << ", tag(s) changed:" << endl << " " << targetRoom->tagNames().join(", ").toStdString() << endl; if (targetRoom->tags().contains(TestTag)) { cout << "Test tag set, removing it now" << endl; targetRoom->removeTag(TestTag); QMC_CHECK("Tagging test", !targetRoom->tags().contains(TestTag)); disconnect(targetRoom, &Room::tagsChanged, nullptr, nullptr); } }); cout << "Adding a tag" << endl; targetRoom->addTag(TestTag); } void QMCTest::sendAndRedact() { running.push_back("Redaction"); cout << "Sending a message to redact" << endl; auto txnId = targetRoom->postPlainText(origin % ": message to redact"); if (txnId.isEmpty()) { QMC_CHECK("Redaction", false); return; } connect(targetRoom, &Room::messageSent, this, [this, txnId](const QString& tId, const QString& evtId) { if (tId != txnId) return; cout << "Redacting the message" << endl; targetRoom->redactEvent(evtId, origin); connectUntil(targetRoom, &Room::addedMessages, this, [this, evtId] { return checkRedactionOutcome(evtId); }); }); } bool QMCTest::checkRedactionOutcome(const QString& evtIdToRedact) { // There are two possible (correct) outcomes: either the event comes already // redacted at the next sync, or the nearest sync completes with // the unredacted event but the next one brings redaction. auto it = targetRoom->findInTimeline(evtIdToRedact); if (it == targetRoom->timelineEdge()) return false; // Waiting for the next sync if ((*it)->isRedacted()) { cout << "The sync brought already redacted message" << endl; QMC_CHECK("Redaction", true); } else { cout << "Message came non-redacted with the sync, waiting for redaction" << endl; connectUntil(targetRoom, &Room::replacedEvent, this, [this, evtIdToRedact](const RoomEvent* newEvent, const RoomEvent* oldEvent) { if (oldEvent->id() != evtIdToRedact) return false; QMC_CHECK("Redaction", newEvent->isRedacted() && newEvent->redactionReason() == origin); return true; }); } return true; } void QMCTest::markDirectChat() { if (targetRoom->directChatUsers().contains(c->user())) { cout << "Warning: the room is already a direct chat," " only unmarking will be tested" << endl; checkDirectChatOutcome({ { c->user(), targetRoom->id() } }); return; } // Connect first because the signal is emitted synchronously. connect(c.data(), &Connection::directChatsListChanged, this, &QMCTest::checkDirectChatOutcome); cout << "Marking the room as a direct chat" << endl; c->addToDirectChats(targetRoom, c->user()); } void QMCTest::checkDirectChatOutcome(const Connection::DirectChatsMap& added) { running.push_back("Direct chat test"); disconnect(c.data(), &Connection::directChatsListChanged, nullptr, nullptr); if (!targetRoom->isDirectChat()) { cout << "The room has not been marked as a direct chat" << endl; QMC_CHECK("Direct chat test", false); return; } if (!added.contains(c->user(), targetRoom->id())) { cout << "The room has not been listed in new direct chats" << endl; QMC_CHECK("Direct chat test", false); return; } cout << "Unmarking the direct chat" << endl; c->removeFromDirectChats(targetRoom->id(), c->user()); QMC_CHECK("Direct chat test", !c->isDirectChat(targetRoom->id())); } void QMCTest::conclude() { c->stopSync(); auto succeededRec = QString::number(succeeded.size()) + " tests succeeded"; if (!failed.isEmpty() || !running.isEmpty()) succeededRec += " of " % QString::number(succeeded.size() + failed.size() + running.size()) % " total"; QString plainReport = origin % ": Testing complete, " % succeededRec; QString color = failed.isEmpty() && running.isEmpty() ? "00AA00" : "AA0000"; QString htmlReport = origin % ": Testing complete, " % succeededRec; if (!failed.isEmpty()) { plainReport += "\nFAILED: " % failed.join(", "); htmlReport += "
    Failed: " % failed.join(", "); } if (!running.isEmpty()) { plainReport += "\nDID NOT FINISH: " % running.join(", "); htmlReport += "
    Did not finish: " % running.join(", "); } cout << plainReport.toStdString() << endl; if (targetRoom) { // TODO: Waiting for proper futures to come so that it could be: // targetRoom->postHtmlText(...) // .then(this, &QMCTest::finalize); // Qt-style or // .then([this] { finalize(); }); // STL-style auto txnId = targetRoom->postHtmlText(plainReport, htmlReport); connect(targetRoom, &Room::messageSent, this, [this, txnId](QString serverTxnId) { if (txnId != serverTxnId) return; cout << "Leaving the room" << endl; connect(targetRoom->leaveRoom(), &BaseJob::finished, this, &QMCTest::finalize); }); } else finalize(); } void QMCTest::finalize() { cout << "Logging out" << endl; c->logout(); connect(c.data(), &Connection::loggedOut, qApp, [this] { QCoreApplication::processEvents(); QCoreApplication::exit(failed.size() + running.size()); }); } int main(int argc, char* argv[]) { QCoreApplication app(argc, argv); if (argc < 4) { cout << "Usage: qmc-example " "[ [origin]]" << endl; return -1; } cout << "Connecting to the server as " << argv[1] << endl; auto conn = new Connection; conn->connectToServer(argv[1], argv[2], argv[3]); QMCTest test { conn, argc >= 5 ? argv[4] : nullptr, argc >= 6 ? argv[5] : nullptr }; return app.exec(); } spectral/include/libQuotient/examples/CMakeLists.txt0000644000175000000620000000445613566674122022675 0ustar dilingerstaffcmake_minimum_required(VERSION 3.1) # This CMakeLists file assumes that the library is installed to CMAKE_INSTALL_PREFIX # and ignores the in-tree library code. You can use this to start work on your own client. project(qmc-example CXX) include(CheckCXXCompilerFlag) if (NOT WIN32) include(GNUInstallDirs) endif(NOT WIN32) # Find includes in corresponding build directories set(CMAKE_INCLUDE_CURRENT_DIR ON) # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) # Set a default build type if none was specified if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to 'Debug' as none was specified") set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build" FORCE) # Set the possible values of build type for cmake-gui set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() if (NOT CMAKE_INSTALL_LIBDIR) set(CMAKE_INSTALL_LIBDIR ".") endif() if (NOT CMAKE_INSTALL_BINDIR) set(CMAKE_INSTALL_BINDIR ".") endif() if (NOT CMAKE_INSTALL_INCLUDEDIR) set(CMAKE_INSTALL_INCLUDEDIR "include") endif() set(CMAKE_CXX_STANDARD 14) foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu-zero-variadic-macro-arguments) CHECK_CXX_COMPILER_FLAG("-W${FLAG}" WARN_${FLAG}_SUPPORTED) if ( WARN_${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "(^| )-W?${FLAG}($| )") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W${FLAG}") endif () endforeach () find_package(Qt5 5.6 REQUIRED Network Gui Multimedia) get_filename_component(Qt5_Prefix "${Qt5_DIR}/../../../.." ABSOLUTE) find_package(Quotient REQUIRED) get_filename_component(QMC_Prefix "${Quotient_DIR}/../.." ABSOLUTE) message( STATUS "qmc-example configuration:" ) if (CMAKE_BUILD_TYPE) message( STATUS " Build type: ${CMAKE_BUILD_TYPE}") endif(CMAKE_BUILD_TYPE) message( STATUS " Compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) message( STATUS " Qt: ${Qt5_VERSION} at ${Qt5_Prefix}" ) message( STATUS " Quotient: ${Quotient_VERSION} at ${QMC_Prefix}" ) set(example_SRCS qmc-example.cpp) add_executable(qmc-example ${example_SRCS}) target_link_libraries(qmc-example Qt5::Core Quotient) # Installation install (TARGETS qmc-example RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) spectral/include/libQuotient/mime/0002755000175000000620000000000013566674122017237 5ustar dilingerstaffspectral/include/libQuotient/mime/packages/0002755000175000000620000000000013566674122021015 5ustar dilingerstaffspectral/include/libQuotient/mime/packages/freedesktop.org.xml0000644000175000000620001020761713566674122024654 0ustar dilingerstaff ]> Atari 2600 Atari 7800 ATK inset شكل ATK Ustaŭka ATK Сбор — ATK ATK inset vložka ATK ATK-indsættelse ATK-Inset Ένθετο ATK ATK inset inserción ATK ATK sartzapena ATK-osio ATK innskot encart ATK intlis ATK conxunto ATK תוספת ATK ATK umetak ATK betét Folio intercalari ATK Inset ATK Inset ATK ATK インセット ATK беті ATK inset ATK inset ATK ielaidums ATK-innsats ATK-invoegsel ATK-innskot encart ATK Wstawka ATK Suplemento ATK Conjunto de entrada do ATK Inset ATK вкладка ATK Vložka ATK Vložka ATK Inset ATK АТК уметак ATK-inlägg ATK iç metni вкладка ATK Bộ dát ATK ATK 嵌入对象 ATK 內嵌 ATK Andrew Toolkit electronic book document مستند كتاب إلكتروني elektronnaja kniha Документ — електронна книга document de llibre electrònic dokument elektronické knihy elektronisk bogdokument Elektronisches Buch Έγγραφο ηλεκτρονικού βιβλίου electronic book document documento de libro electrónico liburu elektronikoaren dokumentua elektroninen kirja elektroniskbóka skjal document livre électronique leabhar leictreonach documento de libro electrónico מסמך מסוג ספר אלקטרוני dokument elektroničke knjige elektronikus könyvdokumentum Documento de libro electronic dokumen buku elektronik Documento libro elettronico 電子ブックドキュメント электронды кітабы 전자책 문서 elektroninės knygos dokumentas elektroniskās grāmatas dokuments elektronisch boek elektronisk bok-dokument document libre electronic Dokument książki elektronicznej documento de livro eletrónico Documento de livro eletrônico document carte electronică электронная книга Dokument elektronickej knihy dokument elektronske knjige Dokument libri elektronik документ електронске књиге elektroniskt bokdokument elektronik kitap belgesi документ електронної книги tài liệu cuốn sách điện tử 电子书文档 電子書文件 Adobe Illustrator document مستند أدوبي المصور Dakument Adobe Illustrator Документ — Adobe Illustrator document d'Adobe Illustrator dokument Adobe Illustrator Adobe Illustrator-dokument Adobe-Illustrator-Dokument Έγγραφο Adobe Illustrator Adobe Illustrator document dokumento de Adobe Illustrator documento de Adobe Illustrator Adobe Illustrator dokumentua Adobe Illustrator -asiakirja Adobe Illustrator skjal document Adobe Illustrator cáipéis Adobe Illustrator documento de Adobe Ilustrator מסמך Adobe Ill Adobe Illustrator dokument Adobe Illustrator-dokumentum Documento Adobe Illustrator Dokumen Adobe Illustrator Documento Adobe Illustrator Adobe Illustrator ドキュメント Adobe Illustrator-ის დოკუმენტი Adobe Illustrator құжаты Adobe Illustrator 문서 Adobe Illustrator dokumentas Adobe Illustrator dokuments Dokumen Adobe Illustrator Adobe Illustrator-dokument Adobe Illustrator-document Adobe Illustrator-dokument document Adobe Illustrator Dokument Adobe Illustrator documento Adobe Illustrator Documento do Adobe Illustrator Document Adobe Illustrator документ Adobe Illustrator Dokument Adobe Illustrator Dokument Adobe Illustrator Dokument Adobe Illustrator документ Адобе Илустратора Adobe Illustrator-dokument Adobe Illustrator belgesi документ Adobe Illustrator Tài liệu Adobe Illustrator Adobe Illustrator 文档 Adobe Illustrator 文件 Macintosh BinHex-encoded file ملف Macintosh BinHex مشفر Macintosh BinHex-kodlanmış fayl Fajł Macintosh, BinHex-zakadavany Файл — кодиран във формат BinHex за Macintosh fitxer amb codificació BinHex de Macintosh soubor kódovaný pomocí Macintosh BinHex Ffeil BinHex-amgodwyd Macintosh Macintosh BinHex-kodet fil Macintosh-Datei (BinHex-kodiert) Αρχείο Macintosh κωδικοποίησης BinHex Macintosh BinHex-encoded file dosiero kodigita laŭ Macintosh BinHex archivo Macintosh codificado con BinHex Macintosh BinHex-ekin kodetutako fitxategia Macintosh BinHex -koodattu tiedosto Macintosh BinHex-bronglað fíla fichier codé Macintosh BinHex comhad ionchódaithe le Macintosh BinHex ficheiro de Macintosh codificado con BinHex קובץ מסוג Macintosh BinHex-encoded Macintosh BinHex-kodirana datoteka Macintosh BinHex kódolású fájl File codificate in BinHex de Macintosh Berkas tersandi Macintosh BinHex File Macintosh codificato BinHex Macintosh BinHex エンコードファイル Macintosh BinHex кодталған файлы 매킨토시 BinHex 인코딩된 압축 파일 Macintosh BinHex-encoded failas Macintosh BinHex-kodēts datne Fail terenkod-BinHex Macintosh Macintosh BinHe-kodet arkiv Macintosh BinHex-gecodeerd bestand Macintosh BinHex-koda fil fichièr encodat Macintosh BinHex Zakodowany w BinHex plik Macintosh ficheiro codificado em BinHex de Macintosh Arquivo do Macintosh codificado com BinHex Fișier codat Macintosh BinHex файл (закодированный Macintosh BinHex) Súbor kódovaný pomocou Macintosh BinHex Kodirana datoteka Macintosh (BinHex) File Macintosh i kodifikuar BinHex Мекинтошова БинХекс-кодирана датотека Macintosh BinHex-kodad fil Macintosh BinHex-şifreli dosya файл закодований Macintosh BinHex Tập tin đã mã hoá BinHex của Macintosh Macintosh BinHex 编码的文件 Macintosh BinHex 編碼檔 Mathematica Notebook مذكرة رياضيات Natatnik Mathematica Тетрадка — Mathematica llibreta de notes de Mathematica sešit Mathematica Mathematica Notebook Mathematica-Dokument Σημειωματάριο Mathematica Mathematica Notebook libreta de Mathematica Mathematica Notebook Mathematica-muistilehtiö Mathematica skriviblokkur carnet de notes Mathematica leabhar nótaí Mathematica notebook de Mathematica מחברת מתמטיקה Matematička bilježnica Mathematica notesz Carnet de notas Mathematica Mathematica Notebook Notebook Mathematica Mathematica ノートブック Mathematica Notebook Mathematica 노트북 Mathematica užrašinė Mathematica bloknots Mathematica notisblokk Mathematica-notitieboek Mathematica-notatbok quasernet de nòtas Mathematica Notatnik Mathematica Bloco notas Mathematica Caderno do Mathematica Carnețel Mathematica Mathematica Notebook Zošit Mathematica Datoteka dokumenta Mathematica Notebook matematike бележница Математике Mathematica Notebook-dokument Mathematica Defteri математичний записник Cuốn vở Mathematica Mathematica 记事 Mathematica Notebook MathML document مستند MathML MathML sənədi Dakument MathML Документ — MathML document MathML dokument MathML Dogfen MathML MathML-dokument MathML-Dokument Έγγραφο MathML MathML document MathML-dokumento documento MathML MathML dokumentua MathML-asiakirja MathML skjal document MathML cáipéis MathML documento de MathML מסמך MathML MathML dokument MathML-dokumentum Documento MathML Dokumen MathML Documento MathML MathML ドキュメント MathML-ის დოკუმენტი MathML құжаты MathML 문서 MathML dokumentas MathML dokuments Dokumen MathML MathML-dokument MathML-document MathML-dokument document MathML Dokument MathML documento MathML Documento do MathML Document MathML документ MathML Dokument MathML Dokument MathML Dokument MathML МатМЛ документ MathML-dokument MathML belgesi документ MathML Tài liệu MathML MathML 文档 MathML 文件 MathML Mathematical Markup Language mailbox file ملف صندوق البريد fajł paštovaj skryni Файл — Mailbox fitxer mailbox soubor mailbox postkassefil Mailbox-Datei Αρχείο mailbox mailbox file archivo de buzón de correo mailbox fitxategia mailbox-tiedosto postkassafíla fichier boîte aux lettres comhad bhosca poist ficheiro de caixa de correo קובץ תיבת-דואר datoteka poštanskog sandučića mailbox fájl File de cassa postal berkas kotak surat File mailbox メールボックスファイル пошта жәшігінің файлы 메일함 파일 pašto dėžutės failas pastkastītes datne postboksfil mailbox-bestand mailbox-fil fichièr bóstia de letras Plik poczty (Mailbox) ficheiro de caixa de correio Arquivo de caixa de correio fișier căsuță poștală файл почтового ящика Súbor mailbox datoteka poštnega predala File mailbox датотека поштанског сандучета brevlådefil posta kutusu dosyası файл поштової скриньки tập tin hộp thư mailbox 文件 郵箱檔 Metalink file ملف ميتالنك Изтегляне — Metalink fitxer Metalink soubor metalink Metahenvisningsfil Metalink-Datei Αρχείο Metalink Metalink file Metalink-dosiero archivo de Metalink Metaestekaren fitxategia Metalink-tiedosto Metalink fíla fichier metalink comhad Metalink ficheiro Metalink קובץ Metalink Datoteka meta poveznice Metalink fájl File Metalink Berkas Metalink File Metalink Metalink ファイル Metalink файлы Metalink 파일 Metalink failas Metalink datne Metalink bestand fichièr metalink Plik Metalink ficheiro Metalink Arquivo Metalink Fișier Metalink файл Metalink Súbor Metalink Datoteka povezave Metalink датотека метавезе Metalink-fil Metalink dosyası файл метапосилання 元链接文件 Metalink 檔案 Metalink file ملف ميتالنك Изтегляне — Metalink fitxer Metalink soubor metalink Metahenvisningsfil Metalink-Datei Αρχείο Metalink Metalink file Metalink-dosiero archivo de Metalink Metaestekaren fitxategia Metalink-tiedosto Metalink fíla fichier metalink comhad Metalink ficheiro Metalink קובץ Metalink Datoteka meta poveznice Metalink fájl File Metalink Berkas Metalink File Metalink Metalink ファイル Metalink файлы Metalink 파일 Metalink failas Metalink datne Metalink bestand fichièr metalink Plik Metalink ficheiro Metalink Arquivo Metalink Fișier Metalink файл Metalink Súbor Metalink Datoteka povezave Metalink датотека метавезе Metalink-fil Metalink dosyası файл метапосилання 元链接文件 Metalink 檔案 unknown مجهول nieviadomy Неизвестен тип desconegut neznámý ukendt Unbekannt Άγνωστο unknown nekonate desconocido ezezaguna tuntematon ókent inconnu anaithnid descoñecido לא ידוע nepoznato ismeretlen incognite tak diketahui Sconosciuto 不明 უცნობი белгісіз 알 수 없음 nežinoma nezināms Entah ukjent onbekend ukjend desconegut Nieznany typ desconhecido Desconhecido necunoscut неизвестно Neznámy neznano Nuk njihet непознато okänd bilinmeyen невідомо không rõ 未知 不明 Partially downloaded file fitxer baixat parcialment částečně stažený soubor Delvist hentet fil Teilweise heruntergeladene Datei Μερικώς ληφθέντο αρχείο Partially downloaded file archivo descargado parcialmente Partzialki deskargatutako fitxategia Osittain ladattu tiedosto fichier partiellement téléchargé Ficheiro descargado parcialmente קובץ שהתקבל חלקית Djelomično preuzeta datoteka Részben letöltött fájl File partialmente discargate Berkas yang terunduh sebagian File parzialmente scaricato Жартылай жүктелген файл 일부 다운로드한 파일 fichièr parcialament telecargat Częściowo pobrany plik Ficheiro parcialmente transferido Arquivo baixado parcialmente Частично загруженный файл Čiastočne stiahnutý súbor Delno prenesena datoteka делимично преузета датотека Delvis hämtad fil Kısmen indirilmiş dosya частково отриманий файл 下载的部分文件 已部份下載的檔案 ODA document مستند ODA ODA sənədi Dakument ODA Документ — ODA document ODA dokument ODA Dogfen ODA ODA-dokument ODA-Dokument Έγγραφο ODA ODA document ODA-dokumento documento ODA ODA dokumentua ODA-asiakirja ODA skjal document ODA cáipéis ODA documento ODA מסמך ODA ODA dokument ODA-dokumentum Documento ODA Dokumen ODA Documento ODA ODA ドキュメント ODA დოკუმენტი ODA құжаты ODA 문서 ODA dokumentas ODA dokuments Dokumen ODA ODA-dokument ODA-document ODA-dokument document ODA Dokument ODA documento ODA Documento ODA Document ODA документ ODA Dokument ODA Dokument ODA Dokument ODA ОДА документ ODA-dokument ODA belgesi документ ODA Tài liệu ODA ODA 文档 ODA 文件 ODA Office Document Architecture WWF document Документ — WWF document WWF dokument WWF WWF-dokument WWF-Dokument Έγγραφο WWF WWF document WWF-dokumento documento WWF WWF dokumentua WWF-asiakirja document WWF documento de WWF מסמך WWF WWF dokument WWF-dokumentum Documento WWF Dokumen WWF Documento WWF WWF 文書 WWF დოკუმენტი WWF құжаты WWF 문서 WWF dokuments WWF document document WWF Dokument WWF documento WWF Documento WWF документ WWF Dokument WWF Dokument WWF ВВФ документ WWF-dokument WWF belgesi документ WWF WWF WWF 文件 PDF document مستند PDF Dakument PDF Документ — PDF document PDF dokument PDF Dogfen PDF PDF-dokument PDF-Dokument Έγγραφο PDF PDF document PDF-dokumento documento PDF PDF dokumentua PDF-asiakirja PDF skjal document PDF cáipéis PDF documento PDF מסמך PDF PDF dokument PDF-dokumentum Documento PDF Dokumen PDF Documento PDF PDF ドキュメント PDF құжаты PDF 문서 PDF dokumentas PDF dokuments Dokumen PDF PDF-dokument PDF-document PDF-dokument document PDF Dokument PDF documento PDF Documento PDF Document PDF документ PDF Dokument PDF Dokument PDF Dokument PDF ПДФ документ PDF-dokument PDF belgesi документ PDF Tài liệu PDF PDF 文档 PDF 文件 PDF Portable Document Format XSPF playlist قائمة تشغيل XSPF Śpis piesień XSPF Списък за изпълнение — XSPF llista de reproducció XSPF seznam k přehrání XSPF XSPF-afspilningsliste XSPF-Wiedergabeliste Λίστα αναπαραγωγής XSPF XSPF playlist XSPF-ludlisto lista de reproducción XSPF XSPF erreprodukzio-zerrenda XSPF-soittolista XSPF avspælingarlisti liste de lecture XSPF seinmliosta XSPF lista de reprodución XSPF רשימת נגינה XSPF XSPF popis za reprodukciju XSPF-lejátszólista Lista de selection XSPF Senarai pular XSPF Playlist XSPF XSPF 再生リスト XSPF ойнау тізімі XSPF 재생 목록 XSPF grojaraštis XSPF repertuārs XSPF-spilleliste XSPF-afspeellijst XSPF-speleliste lista de lectura XSPF Lista odtwarzania XSPF lista de reprodução XSPF Lista de reprodução XSPF Listă XSPF список воспроизведения XSPF Zoznam skladieb XSPF Seznam predvajanja XSPF Listë titujsh XSPF ИксСПФ списак нумера XSPF-spellista XSPF çalma listesi список програвання XSPF Danh mục nhạc XSPF XSPF 播放列表 XSPF 播放清單 XSPF XML Shareable Playlist Format Microsoft Windows theme pack حزمة سمات Microsoft Works Пакет с тема — Microsoft Windows paquet de temes de Microsoft Windows balík motivů Microsoft Windows Microsoft Windows-temapakke Themenpaket für Microsoft Windows Πακέτο θέματος Microsoft Windows Microsoft Windows theme pack paquete de tema para Microsoft Windows Microsoft Windows-en gaiaren paketea Microsoft Windows -teemapaketti Microsoft Windows tema pakki paquet de thèmes Microsoft Windows paca téamaí Microsoft Windows paquete de tema de Microsoft Windows חבילת ערכות נושא של Microsoft Windows Microsoft Windows paket tema Microsoft Windows témacsomag Pacchetto de themas Microsoft Windows Pak tema Microsoft Windows Pacchetto temi Microsoft Windows Microsoft Windows テーマパック Microsoft Windows-ის თემის შეკვრა Microsoft Windows тема дестесі Microsoft Windows 테마 패키지 Microsoft Windows temų paketas Microsoft Windows motīvu paka Microsoft Windows thema pack paquet de tèmas Microsoft Windows Pakiet motywu Microsoft Windows pacote de tema Microsoft Windows Pacote de temas do Microsoft Windows Pachet de teme Microsoft Windows пакет темы Microsoft Windows Balík tém Microsoft Windows Datoteka teme Microsoft Windows пакет теме Мајкрософт Виндоуза Microsoft Windows-temapaket Microsoft Windows tema paketi пакунок з темою Microsoft Windows Microsoft Windows 主题包 微軟視窗佈景主題包 AmazonMP3 download file fitxer baixat d'AmazonMP3 soubor stahování AmazonMP3 AmazonMP3-downloadfil AmazonMP3-Herunterladedatei Αρχείο λήψης AmazonMP3 AmazonMP3 download file archivo de descarga de AmazonMP3 AmazonMP3 deskarga fitxategia fichier téléchargé AmazonMP3 Ficheiro de descarga de AmazonMP3 קובץ הורדת AmazonMP3 AmazonMP3 preuzeta datoteka AmazonMP3 letöltésfájl File de discargamento AmazonMP3 Berkas unduh AmazonMP3 File scaricamento AmazonMP3 AmazonMP3 ダウンロードファイル AmazonMP3 жүктеме файлы AmazonMP3 다운로드 파일 AmazonMP3 lejupielādes datne fichièr telecargat AmazonMP3 Pobrany plik AmazonMP3 ficheiro transferido AmazonMP3 Arquivo de download AmazonMP3 файл загрузки AmazonMP3 Stiahnutý súbor AmazonMP3 Datoteka prenosa AmazonMP3 датотека преузимања АмазонаМП3 AmazonMP3-hämtningsfil AmazonMP3 indirme dosyası файл завантаження AmazonMP3 AmazonMP3 下载文件 AmazonMP3 下載檔 GSM 06.10 audio GSM 06.10 سمعي Аудио — GSM 06.10 àudio de GSM 06.10 zvuk GSM 06.10 GSM 06.10-lyd GSM-06.10-Audio Ήχος GSM 06.10 GSM 06.10 audio sonido GSM 06.10 GSM 06.10 audioa GSM 06.10 -ääni GSM 06.10 ljóður audio GSM 06.10 fuaim GSM 06.10 son de GSM 06.10 שמע GSM 06.10 GSM 06.10 audio GSM 06.10 hang Audio GSM 06.10 Audio GSM 06.10 Audio GSM 06.10 GSM 06.10 オーディオ GSM 06.10 აუდიო GSM 06.10 аудиосы GSM 06.10 오디오 GSM 06.10 garso įrašas GSM 06.10 audio GSM 06.10 audio àudio GSM 06.10 Plik dźwiękowy GSM 06.10 áudio GSM 06.10 Áudio GSM 06.10 GSM 06.10 audio аудио GSM 06.10 Zvuk GSM 06.10 Zvočna datoteka GSM 06.10 ГСМ 06.10 звук GSM 06.10-ljud GSM 06.10 ses dosyası звук GSM 06.10 Âm thanh GSM 06.10 GSM 06.10 音频 GSM 06.10 音訊 GSM Global System for Mobile communications iRiver Playlist قائمة تشغيل iRiver Śpis piesień iRiver Списък за изпълнение — iRiver llista de reproducció iRiver seznam k přehrání iRiver iRiver-afspilningsliste iRiver-Wiedergabeliste Λίστα αναπαραγωγής iRiver iRiver Playlist iRiver-ludlisto lista de reproducción de iRiver iRiver erreprodukzio-zerrenda iRiver-soittolista iRiver avspælingarlisti liste de lecture iRiver seinmliosta iRiver lista de reprodución de iRiver רשימת נגינה של iRiver iRiver popis za reprodukciju iRiver lejátszólista Lista de selection iRiver iRiver Playlist Playlist iRiver iRiver 再生リスト iRiver ойнау тізімі iRiver 재생 목록 iRiver grojaraštis iRiver repertuārs iRiver-spilleliste iRiver-afspeellijst iRiver speleliste lista de lectura iRiver Lista odtwarzania iRiver lista de reprodução iRiver Lista de reprodução do iRiver Listă iRiver список воспроизведения iRiver Zoznam skladieb iRiver Seznam predvajanja iRiver Listë titujsh iRiver иРивер списак нумера iRiver-spellista iRiver Çalma Listesini список програвання iRiver danh mục nhạc iRiver iRiver 播放列表 iRiver 播放清單 PGP/MIME-encrypted message header ترويسة رسالة PGP/MIME-مشفرة Zahałovak paviedamleńnia, zašyfravany ŭ PGP/MIME Заглавна част на шифрирано съобщение — PGP/MIME capçalera de missatge amb xifrat PGP/MIME záhlaví zprávy zašifrované pomocí PGP/MIME PGP-/MIME-krypteret meddelelseshoved PGP/MIME-verschlüsselter Nachrichtenkopf Κεφαλίδα μηνύματος κρυπτογραφημένου κατά PGP/MIME PGP/MIME-encrypted message header PGP/MIME-ĉifrita ĉapo de mesaĝo cabecera de mensaje cifrado PGP/MIME PGP/MIME enkriptatutako mezu-goiburua PGP/MIME-salattu viestiotsikko PGP/MIME-encrypted boð tekshøvd en-tête de message codé PGP/MIME ceanntásc teachtaireachta ionchódaithe le PGP/MIME cabeceira de mensaxe cifrado PGP/MIME כותר של קובץ מוצפן מסוג PGP/MIME PGP/MIME-šrifrirano zaglavlje poruke PGP/MIME titkosított üzenetfejléc Capite de message cryptate con PGP/MIME Tajuk pesan terenkripsi PGP/MIME Intestazione messaggio PGP/MIME-encrypted PGP/MIME 暗号化メッセージヘッダー PGP/MIME-шифрленген мәлімдеме тақырыптамасы PGP/MIME으로 암호화된 메시지 헤더 PGP/MIME užšifruota žinutės antraštė PGP/MIME-šifrēta ziņas galvene Pengepala mesej terenkripsi PGP/MIME PGP/MIME-kryptert meldingshode PGP/MIME-versleutelde berichtkopregels PGP/MIME-kryptert meldingshovud entèsta de messatge encodat PGP/MIME Nagłówek listu zaszyfrowanego PGP/MIME cabeçalho de mensagem encriptada com PGP/MIME Cabeçalho de mensagem criptografada PGP/MIME Antet de mesaj encriptat PGP/MIME заголовок сообщения, зашифрованный PGP/MIME Hlavičke správy zašifrovaná pomocou PGP/MIME Datoteka glave šifriranega sporočila PGP/MIME Header mesazhi të kriptuar PGP/MIME ПГП/МИМЕ шифровано заглавље поруке PGP/MIME-krypterat meddelandehuvud PGP/MIME-şifreli ileti başlığı заголовок шифрованого PGP/MIME повідомлення Phần đầu thông điệp đã mật mã bằng PGP/MIME PGP/MIME 加密的信件头 PGP/MIME 加密訊息標頭 PGP keys مفاتيح PGP PGP açarları Klučy PGP Ключове — PGP claus PGP klíče PGP Allweddi PGP PGP-nøgler PGP-Schlüssel Κλειδιά PGP PGP keys PGP-ŝlosiloj claves PGP PGP giltzak PGP-avainrengas PGP lyklar clés PGP eochracha PGP Chaves PGP מפתחות PGP PGP ključevi PGP-kulcs Claves PGP Kunci PGP Chiavi PGP PGP 鍵 PGP кілттері PGP 키 PGP raktai PGP atslēgas Kekunci PGP PGP-nøkler PGP-sleutels PGP-nøkler claus PGP Klucze PGP chaves PGP Chaves PGP Chei PGP ключи PGP Kľúče PGP Datoteka ključa PGP Kyçe PGP ПГП кључеви PGP-nycklar PGP anahtarları ключі PGP Khoá PGP PGP 密钥 PGP 鑰匙 PGP Pretty Good Privacy detached OpenPGP signature إمضاء OpenPGP مفصول adłučany podpis OpenPGP Отделен подпис — OpenPGP signatura OpenPGP abstreta oddělený podpis OpenPGP frigjort OpenPGP-signatur Isolierte OpenPGP-Signatur Αποκομμένη υπογραφή OpenPGP detached OpenPGP signature dekroĉa OpenPGP-subskribo firma OpenPGP separada desuzturtako OpenPGP sinadura erillinen OpenPGP-allekirjoitus skild OpenPGP undirskrift signature OpenPGP détachée síniú OpenPGP scartha sinatura de OpenPGP independente חתימת OpenPGP מנותקת odvojen OpenPGP potpis leválasztott OpenPGP-aláírás Signatura OpenPGP distachate tanda tangan OpenPGP yang terlepas Firma staccata OpenPGP 分離 OpenPGP 署名 бөлінген OpenPGP қолтаңбасы 분리된 OpenPGP 서명 neprisegtas OpenPGP parašas atvienots OpenPGP paraksts Tandatangan OpenPGP terlerai frakoblet OpenPGP-signatur losse OpenPGP-ondertekening fråkopla OpenPGP-signatur signatura OpenPGP destacada Oddzielony podpis OpenPGP assinatura OpenPGP solta Assinatura OpenPGP destacada semnătură OpenPGP detașată отсоединённая подпись OpenPGP Oddelený podpis OpenPGP odpet podpis OpenPGP Firmë e shkëputur OpenPGP одвојени ОпенПГП потпис frikopplad OpenPGP-signatur müstakil OpenPGP imzası відокремлений OpenPGP підпис chữ ký OpenPGP tách rời 分离的 OpenPGP 签名 分離的 OpenPGP 簽章 PKCS#7 Message or Certificate missatge o certificat PKCS#7 zpráva nebo certifikát PKCS#7 PKCS#7-besked eller certifikat PKCS#7 Nachricht oder Zertifikat Μήνυμα ή πιστοποιητικό PKCS#7 PKCS#7 Message or Certificate mensaje o certificado PKCS#7 PKCS#7 mezu edo zertifikazioa PKCS#7-viesti tai -varmenne Message ou certificat PKCS#7 Mensaxe ou certificado PKCS#7 הודעה או אישור מסוג PKCS#7 PKCS#7 poruka ili vjerodajnica PKCS#7 üzenet vagy tanúsítvány Message o certificato PKCS#7 Sertifikat atau Pesan PKCS#7 Messaggio o certificato PKCS#7 PKCS#7 メッセージまたは証明書 PKCS#7 хабарламасы не сертификаты PKCS#7 메시지 또는 인증서 PKCS#7 ziņojums vai sertifikāts Messatge o certificat PKCS#7 Wiadomość lub certyfikat PKCS#7 Mensagem ou certificado PKCS#7 Certificado ou Mensagem PKCS#7 сообщение или сертификат PKCS#7 Správa alebo certifikát PKCS#7 Sporočilo ali dovoljenje PKCS#7 ПКЦС#7 порука или уверење PKCS#7-meddelande eller -certifikat PKCS#7 İletisi veya Sertifikası повідомлення або сертифікат PKCS#7 PKCS#7 消息或证书 PKCS#7 訊息或憑證 PKCS Public-Key Cryptography Standards detached S/MIME signature إمضاء S/MIME مفصول adłučany podpis S/MIME Отделен подпис — S/MIME signatura S/MIME abstreta oddělený podpis S/MIME frigjort S/MIME-signatur Isolierte S/MIME-Signatur Αποκομμένη υπογραφή S/MIME detached S/MIME signature dekroĉa S/MIME-subskribo firma S/MIME separada desuzturtako S/MIME sinadura erillinen S/MIME-allekirjoitus skild S/MIME undirskrift signature S/MIME détachée síniú S/MIME scartha sinatura S/MIME independente חתימת S/MIME מנותקת odvojen S/MIME potpis leválasztott S/MIME-aláírás Signatura S/MIME distachate tanda tangan S/MIME yang terlepas Firma staccata S/MIME 分離 S/MIME 署名 бөлінген S/MIME қолтаңбасы 분리된 S/MIME 서명 neprisegtas S/MIME parašas atvienots S/MIME paraksts Tandatangan S/MIME terlerai frakoblet S/MIME-signatur losse S/MIME-ondertekening fråkopla S/MIME-signatur signatura S/MIME destacada Oddzielony podpis S/MIME assinatura S/MIME solta Assinatura S/MIME destacada semnătură S/MIME detașată отсоединённая подпись S/MIME Oddelený podpis S/MIME odpet podpis S/MIME Firmë e shkëputur S/MIME одвојени С/МИМЕ потпис frikopplad S/MIME-signatur müstakil S/MIME imzası відокремлений S/MIME підпис chữ ký S/MIME tách rời 分离的 S/MIME 签名 分離的 S/MIME 簽章 S/MIME Secure/Multipurpose Internet Mail Extensions PKCS#8 private key رزمة الشهادة PKCS#8 Ключ, частен — PKCS#8 clau privada PKCS#8 soukromý klíč PKCS#8 PKCS#8-privat nøgle PKCS#8 Geheimer Schlüssel Ιδιωτικό κλειδί PKCS#8 PKCS#8 private key clave privada PCKS#8 PKCS#8 gako pribatua PKCS#8 yksityinen avain PKCS#8 privatur lykil clé privée PKCS#8 eochair phríobháideach PKCS#8 Chave privada PKCS#8 מפתח פרטי של PKCS#8 PKCS#8 privatni ključ PKCS#8 személyes kulcs Clave private PKCS#8 Kunci privat PKCS#8 Chiave privata PKCS#8 PKCS#8 秘密鍵 PKCS#8 меншік кілті PKCS#8 개인 키 PKCS#8 asmeninis raktas PKCS#8 privātā atslēga PKCS#8 private sleutel clau privada PKCS#8 Klucz prywatny PKCS#8 chave privada PKCS#8 Chave privada PKCS#8 Cheie privată PKCS#8 личный ключ PKCS#8 Súkromný kľúč PKCS#8 Datoteka osebnega ključa PKCS#8 ПКЦС#8 лични кључ Privat PKCS#8-nyckel PKCS#8 özel anahtarı закритий ключ PKCS#8 PKCS#8 私钥 PKCS#8 私人金鑰 PKCS Public-Key Cryptography Standards PKCS#10 certification request طلب شهادة PKCS#10 Zapyt sertyfikacyi PKCS#10 Заявка за сертификат — PKCS#10 sol·licitud de certificació PKCS#10 žádost o certifikát PKCS#10 PKCS#10-certifikatanmodning PKCS#10-Zertifikatanfrage Αίτηση πιστοποίησης PKCS#10 PKCS#10 certification request petición de certificados PKCS#10 PKCS#10 ziurtagirien eskaera PKCS#10-varmennepyyntö PKCS#10 váttanarumbøn requête de certification PKCS#10 iarratas dheimhniúchán PKCS#10 Solicitude de certificado PKCS#10 בקשה מוסמכת PLCS#10 PKCS#10 zahtjev vjerodajnice PKCS#10-tanúsítványkérés Requesta de certification PKCS#10 Permintaan sertifikasi PKCS#10 Richiesta certificazione PKCS#10 PKCS#10 証明書署名要求 PKCS#10 сертификацияға сұранымы PKCS#10 인증서 요청 PKCS#10 liudijimų užklausa PKCS#10 sertifikācijas pieprasījums PKCS#10-sertifikatforespørsel PKCS#10-certificatieverzoek PKCS#10-sertifiseringsførespurnad requèsta de certificacion PKCS#10 Żądanie certyfikatu PKCS#10 pedido de certificação PKCS#10 Pedido de certificação PKCS#12 Cerere de certificat PKCS#10 запрос сертификации PKCS#10 Požiadavka na certifikát PKCS#10 Datoteka potrdila PKCS#10 Kërkesë çertifikimi PKCS#10 ПКЦС#10 зхатев уверења PKCS#10-certifikatbegäran PKCS#10 sertifika isteği комплект сертифікатів PKCS#10 Yêu cầu chứng nhận PKCS#10 PKCS#10 认证请求 PKCS#10 憑證請求 PKCS Public-Key Cryptography Standards X.509 certificate شهادة X.509 Сертификат — X.509 certificat X.509 certifikát X.509 X.509-certifikat X.509-Zertifikat Πιστοποιητικό X.509 X.509 certificate certificado X.509 X.509 ziurtagiria X.509-varmenne X.509 prógv certificat X.509 teastas X.509 Certificado X.509 אישור X.509 X.509 certifikat X.509 tanúsítvány Certificato X.509 Sertifikat X.509 Certificato X.509 X.509 証明書 X.509 сертификаты X.509 인증서 X.509 liudijimas X.509 sertifikāts X.509 certificaat certificat X.509 Certyfikat X.509 certificado X.509 Certificado X.509 Certificat X.509 сертификат X.509 Certifikát X.509 Datoteka potrdila X.509 Икс.509 уверење X.509-certifikat X.509 sertifikası сертифікат X.509 X.509 证书 X.509 憑證 Certificate revocation list قائمة إبطال الشهادات Списък с отхвърлени сертификати llista de revocació de certificats seznam odvolaných certifikátů Certifikattilbagekaldelsesliste Liste widerrufener Zertifikate Λίστα ανάκλησης πιστοποιητικού Certificate revocation list lista de revocación de certificados Ziurtagiri-errebokatzeen zerrenda Varmenteiden sulkulista Prógv afturtøkulisti liste de révocation de certificat liosta teastas cúlghairmthe lista de certificados de revogación רשימת אישורים מבוטלים popis povučenih certifikata Tanúsítvány-visszavonási lista Lista de revocation de certificatos Daftar pencabutan sertificat (CRL) Elenco certificati di revoca 証明書失効リスト Сертификатты қайта шақыру тізімі 인증서 철회 목록 Panaikintų liudijimų sąrašas Sertifikātu atsaukšanu saraksts Certificaat revocation lijst lista de revocacion de certificat Lista unieważnień certyfikatów lista de revogação de certificados Lista de revogação de certificado Listă de revocare a certificatelor Список аннулирования сертификатов Zoznam zrušených certifikátov Datoteka seznama preklica potrdil списак повлачења уверења Spärrlista för certifikat Sertifika iptal listesi список відкликання сертифікатів 证书吊销列表 憑證撤銷清單 PkiPath certification path مسار شهادة PkiPath Сертификационна верига — PkiPath camí cap a la certificació PkiPath cesta k certifikátu PkiPath PkiPath-certifikationssti PkiPath-Zertifikatspfad Διαδρομή πιστοποιητικού PkiPath PkiPath certification path ruta de certificación PkiPath PkiPath ziurtagirien bide-izena PkiPath-varmennepolku PkiPath váttanleið chemin de certification PkiPath conair dheimhniúcháin PkiPath Ruta de certificación PkiPath נתיב מאושר של PkiPath PkiPath putanja vjerodajnice PkiPath tanúsítványútvonal Cammino de certification PkiPath Alamat sertifikasi PkiPath Percorso certificazione PkiPath PkiPath 証明書パス PkiPath сертификаттау жолы PkiPath 인증서 요청 PkiPath liudijimų maršrutas PkiPath sertifikāta ceļš PkiPath-certificatiepad camin de certificacion PkiPath Ścieżka certyfikacji PkiPath caminho de certificação PkiPath Pedido de certificação PkiPath Cale certificare PkiPath путь сертификации PkiPath Cesta k certifikátu PkiPath Datoteka poti potrdila PkiPath путања уверења ПкиПат-а PkiPath-certifikatsekvens PkiPath sertifika yolu шлях сертифікації PkiPath Đường dẫn cấp chứng nhận PkiPath PkiPath 证书目录 PkiPath 憑證路徑 PS document مستند PS Dakument PS Документ — PS document PS dokument PS PS-dokument PS-Dokument Έγγραφο PS PS document PS-dokumento documento PS PS dokumentua PS-asiakirja PS skjal document PS cáipéis PS documento PS מסמך PS PS dokument PS dokumentum Documento PS Dokumen PS Documento PS PS ドキュメント PS құжаты PS 문서 PS dokumentas PS dokuments PS-dokument PS-document PS-dokument document PS Dokument PS documento PS Documento PS Document PS документ PS Dokument PS Dokument PS Dokument PS ПС документ PS-dokument PS belgesi документ PS Tài liệu PS PS 文档 Ps 文件 PS PostScript Plucker document مستند Plucker Dakument Plucker Документ — Plucker document Plucker dokument Plucker Pluckerdokument Plucker-Dokument Έγγραφο Plucker Plucker document Plucker-dokumento documento de Plucker Plucker dokumentua Plucker-asiakirja Plucker skjal document Plucker cáipéis Plucker documento de Plucker מסמך של Plucker Plucker dokument Plucker dokumentum Documento Plucker Dokumen Plucker Documento Plucker Plucker ドキュメント Plucker құжаты Plucker 문서 Plucker dokumentas Plucker dokuments Plucker-dokument Plucker-document Plucker-dokument document Plucker Dokument Plucker documento Plucker Documento do Plucker Document Plucker документ Plucker Dokument Plucker Dokument Plucker Dokument Plucker Плакер документ Plucker-dokument Plucker belgesi документ Plucker Tài liệu Plucker Plucker 文档 Plucker 文件 RAML document RAML RESTful API Modeling Language RELAX NG XML schema مخطط RELAX NG XML Схема за XML — RELAX NG esquema XML en RELAX NG schéma RELAX NG XML RELAX NG XML-skema RELAX NG XML-Schema Σχήμα RELAX NG XML RELAX NG XML schema esquema XML RELAX NG RELAX NG XML eskema RELAX NG XML-skeema schéma XML RELAX NG scéimre XML RELAX NG Esquema XML RELAX NG סכנת RELAX NG XML RELAX NG XML shema RELAX NG XML-séma Schema XML RELAX NG Skema XML RELAX NG Schema XML RELAX NG RELAX NG XML スキーマ RELAX NG XML сұлбасы RELAX NG XML 스키마 RELAX NG XML schema RELAX NG XML shēma RELAX NG XML schema esquèma XML RELAX NG Schemat XML RELAX NG Esquema RELAX NG XML Esquema XML de RELAX NG Schemă RELAX NG XML XML-схема RELAX NG XML schéma RELAX NG Datoteka shema RELAX NG XML РЕЛАКС НГ ИксМЛ шема RELAX NG XML-schema RELAX NG XML şeması XML-схема RELAX NG RELAX NG XML 模式 RELAX NG XML schema RELAX NG REgular LAnguage for XML Next Generation RTF document مستند RTF Dakument RTF Документ — RTF document RTF dokument RTF RTF-dokument RTF-Dokument Έγγραφο RTF RTF document RTF-dokumento documento RTF RTF dokumentua RTF-asiakirja RTF skjal document RTF cáipéis RTF documento RTF מסמך RTF RTF dokument RTF dokumentum Documento RTF Dokumen RTF Documento RTF RTF ドキュメント RTF құжаты RTF 문서 RTF dokumentas RTF dokuments RTF-dokument RTF-document TRF-dokument document RTF Dokument RTF documento RTF Documento RTF Document RTF документ RTF Dokument RTF Dokument RTF Dokument RTF РТФ документ RTF-dokument RTF belgesi документ RTF Tài liệu RTF RTF 文档 RTF 文件 RTF Rich Text Format Sieve mail filter script سكربت مرشح بريد Sieve Skrypt filtravańnia pošty Sieve Скрипт-филтър за пресяване на поща script de filtre de correu Sieve skript poštovního filtru Sieve Sieve e-post-filterprogram Sieve-E-Mail-Filterskript Δέσμη ενεργειών φιλτραρίσματος αλληλογραφίας Sieve Sieve mail filter script secuencia de órdenes de filtro en Sieve Sieve posta-iragazki script-a Sieve-postinsuodatuskomentotiedosto script de filtrage de courriel Sieve script scagaire phost Sieve Script de filtro de correo Sieve תסריט סינון דואר של Sieve Sieve skripta filtriranja pošte Sieve levélszűrő parancsfájl Script de filtration de e-mail Sieve Skrip filter surat Sieve Script filtro posta Sieve Sieve メールフィルタスクリプト Sieve пошталық фильтр сценарийі Sieve 메일 필터 스크립트 Sieve pašto filtro scenarijus Sieve pasta filtra skripts Sieve e-postfilter skript Sieve mailfilter-script Sieve e-postfilterskript escript de filtratge de corrièr electronic Sieve Skrypt filtra poczty Sieve Script de filtragem de correio Sieve Script de filtro de mensagens do Sieve Script filtrare email Sieve сценарий почтового фильтра Sieve Skript poštového filtra Sieve Skriptna datoteka Sieve poštnega filtra Script filtrim poste Sieve Сјев скрипта пропусника поште Sieve-epostfilterskript Sieve posta filtre betiği скрипт поштового фільтру Sieve Văn lệnh lọc thư Sieve Sieve 邮件过滤脚本 Sieve 郵件過濾指令稿 SMIL document مستند SMIL Dakument SMIL Документ — SMIL document SMIL dokument SMIL SMIL-dokument SMIL-Dokument Έγγραφο SMIL SMIL document SMIL-dokumento documento SMIL SMIL dokumentua SMIL-asiakirja SMIL skjal document SMIL cáipéis SMIL documento SMIL מסמך SMIL SMIL dokument SMIL dokumentum Documento SMIL Dokumen SMIL Documento SMIL SMIL ドキュメント SMIL құжаты SMIL 문서 SMIL dokumentas SMIL dokuments SMIL-dokument SMIL-document SMIL-dokument document SMIL Dokument SMIL documento SMIL Documento SMIL Document SMIL документ SMIL Dokument SMIL Dokument SMIL Dokument SMIL СМИЛ документ SMIL-dokument SMIL belgesi документ SMIL Tài liệu SMIL SMIL 文档 SMIL 文件 SMIL Synchronized Multimedia Integration Language WPL playlist قائمة تشغيل WPL Списък за изпълнение — WPL llista de reproducció WPL seznam k přehrání WPL WPL-afspilningsliste WPL-Wiedergabeliste Λίστα αναπαραγωγής WPL WPL playlist WPL-ludlisto lista de reproducción WPL WPL erreprodukzio-zerrenda WPL-soittolista WPL avspælingarlisti liste de lecture WPL seinmliosta WPL lista de reprodución WPL רשימת נגינה WPL WPL popis za reprodukciju WPL-lejátszólista Lista de selection WPL Senarai putar WPL Playlist WPL WPL 再生リスト WPL ойнау тізімі WPL 재생 목록 WPL grojaraštis WPL repertuārs WPL-afspeellijst lista de lectura WPL Lista odtwarzania WPL lista de reprodução WPL Lista de reprodução do WPL Listă redare WPL список воспроизведения WPL Zoznam skladieb WPL Seznam predvajanja WPL ВПЛ списак нумера WPL-spellista WPL çalma listesi список відтворення WPL Danh mục nhạc WPL WPL 播放列表 WPL 播放清單 WPL Windows Media Player Playlist SQLite2 database قاعدة بيانات SQLite2 Baza źviestak SQLite2 База от данни — SQLite2 base de dades SQLite2 databáze SQLite2 SQLite2-database SQLite2-Datenbank Βάση δεδομένων SQLite2 SQLite2 database SQLite2-datumbazo base de datos SQLite2 SQLite2 datu-basea SQLite2-tietokanta SQLite2 dátustovnur base de données SQLite2 bunachar sonraí SQLite2 base de datos SQLite2 מסד נתונים מסוג SQLite2 SQLite2 baza podataka SQLite2 adatbázis Base de datos SQLite2 Basis data SQLite2 Database SQLite2 SQLite2 データベース SQLite2 дерекқоры SQLite2 데이터베이스 SQLite2 duomenų bazė SQLite2 datubāze SQLite2-database SQLite2-gegevensbank SQLite2-database banca de donadas SQLite2 Baza danych SQLite2 base de dados SQLite2 Banco de dados SQLite2 Bază de date SQLite2 база данных SQLite2 Databáza SQLite2 Podatkovna zbirka SQLite2 Bazë me të dhëna SQLite2 СКуЛајт2 база података SQLite2-databas SQLite2 veritabanı База даних SQLite2 Cơ sở dữ liệu SQLite2 SQLite2 数据库 SQLite2 資料庫 SQLite3 database قاعدة بيانات SQLite3 Baza źviestak SQLite3 База от данни — SQLite3 base de dades SQLite3 databáze SQLite3 SQLite3-database SQLite3-Datenbank Βάση δεδομένων SQLite3 SQLite3 database SQLite3-datumbazo base de datos SQLite3 SQLite3 datu-basea SQLite3-tietokanta SQLite3 dátustovnur base de données SQLite3 bunachar sonraí SQLite3 base de datos SQLite3 מסד נתונים מסוג SQLite3 SQLite3 baza podataka SQLite3 adatbázis Base de datos SQLite3 Basis data SQLite3 Database SQLite3 SQLite3 データベース SQLite3 дерекқоры SQLite3 데이터베이스 SQLite3 duomenų bazė SQLite3 datubāze SQLite3-database SQLite3-gegevensbank SQLite3-database banca de donadas SQLite3 Baza danych SQLite3 base de dados SQLite3 Banco de dados SQLite3 Bază de date SQLite3 база данных SQLite3 Databáza SQLite3 Podatkovna zbirka SQLite3 Bazë me të dhëna SQLite3 СКуЛајт3 база података SQLite3-databas SQLite3 veritabanı база даних SQLite3 Cơ sở dữ liệu SQLite3 SQLite3 数据库 SQLite3 資料庫 GEDCOM family history تاريخ عائلة GEDCOM Siamiejnaja historyja GEDCOM Родословно дърво — GEDCOM antecedents familiars GEDCOM rodokmen GEDCOM GEDCOM-familiehistorie GEDCOM-Stammbaum Οικογενειακό ιστορικό GEDCOM GEDCOM family history historial familiar de GEDCOM GEDCOM famili historia GEDCOM-sukuhistoria GEDCOM familjusøga généalogie GEDCOM stair theaghlach GEDCOM historial de familia GEDCOM היסטוריה משפחתית של GEDCOM GEDCOM obiteljska povijest GEDCOM családtörténet Genealogia GEDCOM Sejarah keluarga GEDCOM Cronologia famiglia GEDCOM GEDCOM 家系図データ GEDCOM ოჯახის ისტორია GEDCOM отбасы тарихы GEDCOM 가족 내력 GEDCOM šeimos istorija GEDCOM ģimenes vēsture GEDCOM-familiehistorikk GEDCOM-stamboom GEDCOM-familehistorie genealogia GEDCOM Plik historii rodziny GEDCOM história familiar GEDCOM Histórico familiar do GEDCOM Tablou genealogic GEDCOM история семьи GEDCOM Rodokmeň GEDCOM Datoteka družinske zgodovine GEDCOM Kronollogji familje GEDCOM ГЕДКОМ историјат породице GEDCOM-släktträd GEDCOM aile geçmişi історія родини GEDCOM Lịch sử gia đình GEDCOM GEDCOM 家谱 GEDCOM 家族史 GEDCOM GEnealogical Data COMmunication Flash video Flash مرئي Videa Flash Видео — Flash vídeo de Flash video Flash Flashvideo Flash-Video Βίντεο Flash Flash video Flash-video vídeo Flash Flash bideoa Flash-video Flash video vidéo Flash físeán Flash vídeo Flash וידאו של פלאש Flash video Flash videó Video Flash Video Flash Video Flash Flash 動画 Flash-ის ვიდეო Flash видеосы Flash 동영상 Flash vaizdo įrašas Flash video Flash-film Flash-video Flash-video vidèo Flash Plik wideo Flash vídeo Flash Vídeo Flash Video Flash видео Flash Video Flash Video datoteka Flash Video Flash Флеш видео Flash-video Flash video відеокліп Flash Ảnh động Flash Flash 影片 Flash 視訊 JavaFX video Видео — JavaFX vídeo de JavaFX video JavaFX JavaFX-video JavaFX-Video Βίντεο JavaFX JavaFX video JavaFX-video vídeo JavaFX JavaFX bideoa JavaFX-video JavaFX video vidéo JavaFX físeán JavaFX vídeo JavaFX וידאו JavaFX JavaFX video JavaFX videó Video JavaFX Video JavaFX Video JavaFX JavaFX 動画 JavaFX аудиосы JavaFX 동영상 JavaFX video JavaFX video vidèo JavaFX Plik wideo JavaFX vídeo JavaFX Vídeo JavaFX Video JavaFX видео JavaFX Video JavaFX Video JavaFX ЈаваФИкс видео JavaFX-video JavaFX video відеокліп JavaFX JavaFX 视频 JavaFX 視訊 SGF record تسجيلة SGF Zapisanaja hulnia SGF Запис — SGF registre SGF nahrávka SGF SGF-optagelse SGF-Aufzeichnung Εγγραφή SGF SGF record grabación SGF SGF erregistroa SGF-nauhoitus SGF met partie SGF taifead SGF Grabación SGF הקלטת SGF SGF zapis SGF pontszám Partita SGF Catatan SGF Registrazione SGF SGF レコード SGF жазбасы SGF 기록 파일 SGF įrašas SGF ieraksts SGF-oppføring SGF-record SGF-logg partida SGF Zapis gry SGF gravação SGF Gravação SGF Înregistrare SGF запись SGF Záznam SGF Datoteka shranjene igre SGF Regjistrim SGF СГФ запис SGF-protokoll SGF kaydı запис SGF Mục ghi SGF SGF 记录 SGF 紀錄 SGF Smart Game Format XLIFF translation file ملف ترجمة XLIFF Fajł pierakładu XLIFF Превод — XLIFF fitxer de traducció XLIFF soubor překladu XLIFF XLIFF-oversættelsesfil XLIFF-Übersetzung Αρχείο μετάφρασης XLIFF XLIFF translation file archivo de traducción XLIFF XLIFF itzulpen-fitxategia XLIFF-käännöstiedosto XLIFF týðingarfíla fichier de traduction XLIFF comhad aistrithe XLIFF ficheiro de tradución XLIFF קובץ תרגום CLIFF XLIFF datoteka prijevoda XLIFF fordítási fájl File de traduction XLIFF Berkas terjemahan XLIFF File traduzione XLIFF XLIFF 翻訳ファイル XLIFF аударма файлы XLIFF 번역 파일 XLIFF vertimo failas XLIFF tulkošanas datne XLIFF-oversettelsesfil XLIFF-vertalingsbestand XLIFF-omsetjingsfil fichièr de traduccion XLIFF Plik tłumaczenia XLIFF ficheiro de tradução XLIFF Arquivo de tradução XLIFF Fișier de traducere XLIFF файл перевода XLIFF Súbor prekladu XLIFF Datoteka prevoda XLIFF File përkthimesh XLIFF ИксЛИФФ датотека превода XLIFF-översättningsfil XLIFF çeviri dosyası файл перекладу XLIFF Tập tin dịch XLIFF XLIFF 消息翻译文件 XLIFF 翻譯檔 XLIFF XML Localization Interchange File Format YAML document مستند YAML Документ — YAML document YAML dokument YAML YAML-dokument YAML-Dokument Έγγραφο YAML YAML document YAML-dokumento documento YAML YAML dokumentua YAML-asiakirja YAML skjal document YAML cáipéis YAML documento YAML מסמך YAML YAML dokument YAML-dokumentum Documento YAML Dokumen YAML Documento YAML YAML ドキュメント YAML құжаты YAML 문서 YAML dokumentas YAML dokuments YAML document document YAML Dokument YAML documento YAML Documento YAML Document YAML документ YAML Dokument YAML Dokument YAML ЈАМЛ документ YAML-dokument YAML belgesi документ YAML YAML 文档 YAML 文件 YAML YAML Ain't Markup Language Corel Draw drawing تصميم Corel Draw Corel Draw çəkimi Rysunak Corel Draw Чертеж — Corel Draw dibuix de Corel Draw kresba Corel Draw Darlun Corel Draw Corel Draw-tegning Corel-Draw-Zeichnung Σχέδιο Corel Draw Corel Draw drawing grafikaĵo de Corel Draw dibujo de Corel Draw Corel Draw-eko marrazkia Corel Draw -piirros Corel Draw tekning dessin Corel Draw líníocht Corel Draw debuxo de Corel Draw ציור של Corel Draw Corel Draw crtež Corel Draw-rajz Designo Corel Draw Gambar Corel Draw Disegno Corel Draw Corel Draw ドロー Corel Draw-ის ნახაზი Corel Draw суреті Corel Draw 드로잉 Corel Draw piešinys Corel Draw zīmējums Lukisan Corel Draw Corel Draw-tegning Corel Draw-tekening Corel Draw-teikning dessenh Corel Draw Rysunek Corel Draw desenho Corel Drawdesenho Corel Draw Desenho do Corel Draw Desen Corel Draw изображение Corel Draw Kresba Corel Draw Datoteka risbe Corel Draw Vizatim Corel Draw Корел Дров цртеж Corel Draw-teckning Corel Draw çizimi малюнок Corel Draw Bản vẽ Corel Draw Corel Draw 图形 Corel Draw 繪圖 HPGL file ملف HPGL Fajł HPGL Файл — HPGL fitxer HPGL soubor HPGL HPGL-fil HPGL-Datei Αρχείο HPGL HPGL file HPGL-dosiero archivo HPGL HPGL fitxategia HPGL-tiedosto HPGL fíla fichier HPGL comhad HPGL ficheiro HPGL קובץ HGPL HPGL datoteka HPGL fájl File HPGL Berkas HPGL File HPGL HPGL ファイル HPGL файлы HPGL 파일 HPGL failas HPGL datne HPGL-fil HPGL-bestand HPGL-fil fichièr HPGL Plik HPGL ficheiro HPGL Arquivo HPGL Fișier HPGL файл HPGL Súbor HPGL Datoteka HPGL File HPGL ХПГЛ датотека HPGL-fil HPGL dosyası файл HPGL Tập tin HPGL HPGL 文件 HPGL 檔案 HPGL HP Graphics Language PCL file ملف PCL Fajł PCL Файл — PCL fitxer PCL soubor PCL PCL-fil PCL-Datei Αρχείο PCL PCL file PCL-dosiero archivo PCL PCL fitxategia PCL-tiedosto PCL fíla fichier PCL comhad PCL ficheiro PCL קובץ PCL PCL datoteka PCL fájl File PCL Berkas PCL File PCL PCL ファイル PCL файлы PCL 파일 PCL failas PCL datne PCL-fil PCL-bestand PCL-fil fichièr PCL Plik PCL ficheiro PCL Arquivo PCL Fișier PCL файл PCL Súbor PCL Datoteka PCL File PCL ПЦЛ датотека PCL-fil PCL dosyası файл PCL Tập tin PCL PCL 文件 PCL 檔 PCL HP Printer Control Language Lotus 1-2-3 spreadsheet جدول Lotus 1-2-3 Lotus 1-2-3 hesab cədvəli Raźlikovy arkuš Lotus 1-2-3 Таблица — Lotus 1-2-3 full de càlcul de Lotus 1-2-3 sešit Lotus 1-2-3 Taenlen Lotus 1-2-3 Lotus 1-2-3-regneark Lotus-1-2-3-Tabelle Λογιστικό φύλλο Lotus 1-2-3 Lotus 1-2-3 spreadsheet Kalkultabelo de Lotus 1-2-3 hoja de cálculo de Lotus 1-2-3 Lotus 1-2-3 kalkulu-orria Lotus 1-2-3 -taulukko Lotus 1-2-3 rokniark feuille de calcul Lotus 1-2-3 scarbhileog Lotus 1-2-3 folla de cálculo de Lotus 1-2-3 גיליון נתונים של Lotus 1-2-3 Lotus 1-2-3 proračunska tablica Lotus 1-2-3-munkafüzet Folio de calculo Lotus 1-2-3 Lembar sebar Lotus 1-2-3 Foglio di calcolo Lotus 1-2-3 Lotus 1-2-3 スプレッドシート Lotus 1-2-3 электрондық кестесі Lotus 1-2-3 스프레드시트 Lotus 1-2-3 skaičialentė Lotus 1-2-3 izklājlapa Hamparan Lotus 1-2-3 Lotus 1-2-3 regneark Lotus 1-2-3-rekenblad Lotus 1-2-3 rekneark fuèlh de calcul Lotus 1-2-3 Arkusz Lotus 1-2-3 folha de cálculo Lotus 1-2-3 Planilha do Lotus 1-2-3 Foaie de calcul Lotus 1-2-3 электронная таблица Lotus 1-2-3 Zošit Lotus 1-2-3 Preglednica Lotus 1-2-3 Fletë llogaritjesh Lotus 1-2-3 Лотусова 1-2-3 табела Lotus 1-2-3-kalkylblad Lotus 1-2-3 hesap tablosu ел. таблиця Lotus 1-2-3 Bảng tính Lotus 1-2-3 Lotus 1-2-3 工作簿 Lotus 1-2-3 試算表 Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Лотусов Писац Про Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro Lotus Word Pro JET database قاعدة بيانات JET Baza źviestak JET База от данни — JET base de dades JET databáze JET JET-database JET-Datenbank Βάση δεδομένων JET JET database JET-datumbazo base de datos JET JET datu-basea JET-tietokanta JET dátustovnur base de données JET bunachar sonraí JET base de datos JET מסד נתונים מסוג JET JET baza podataka JET adatbázis Base de datos JET Basis data JET Database JET JET データベース JET дерекқоры JET 데이터베이스 JET duomenų bazė JET datubāze JET-database JET-gegevensbank JET-database banca de donadas JET Baza Danych JET base de dados JET Banco de dados JET Bază de date JET база данных JET Databáza JET Podatkovna zbirka JET Bazë me të dhëna JET ЈЕТ база података JET-databas JET veritabanı База даних JET Cơ sở dữ liệu JET JET 数据库 JET 資料庫 JET Joint Engine Technology Microsoft Cabinet archive أرشيف Microsoft Cabinet Архив — Microsoft Cabinet arxiu de Microsoft Cabinet archiv Microsoft Cabinet Microsoft Cabinet-arkiv Microsoft-Cabinet-Archiv Συμπιεσμένο αρχείο Microsoft Cabinet Microsoft Cabinet archive archivador Cabinet de Microsoft Microsoft Cabinet artxiboa Microsoft Cabinet -arkisto Microsoft Cabinet skjalasavn archive Cab Microsoft cartlann Microsoft Cabinet arquivo de Microsoft Cabinet ארכיון CAB (מיקרוסופט) Microsoft Cabinet arhiva Microsoft Cabinet archívum Archivo Microsoft Cabinet Arsip Microsoft Cabinet Archivio Microsoft Cabinet Microsoft Cabinet アーカイブ Microsoft-ის Cabinet არქივი Microsoft Cabinet архиві Microsoft Cabinte 압축 파일 Microsoft Cabinet archyvas Microsoft kabineta arhīvs Microsoft Cabinet-archief archiu Cab Microsoft Archiwum Microsoft Cabinet arquivo Microsoft Cabinet Pacote Cabinet da Microsoft Arhivă Microsoft Cabinet архив Microsoft Cabinet Archív Microsoft Cabinet Datoteka arhiva Microsoft Cabinet Мајкрософтова кабинет архива Microsoft Cabinet-arkiv Microsoft Cabinet arşivi архів Cabinet Microsoft Kho lưu Cabinet Microsoft Microsoft CAB 归档文件 微軟 Cabinet 封存檔 Excel spreadsheet جدول Excel Raźlikovy akruš Excel Таблица — Excel full de càlcul d'Excel sešit Excel Excelregneark Excel-Tabelle Λογιστικό φύλλο Excel Excel spreadsheet Excel-kalkultabelo hoja de cálculo de Excel Excel kalkulu-orria Excel-taulukko Excel rokniark feuille de calcul Excel scarbhileog Excel folla de cálculo de Excel גליון נתונים של אקסל Excel proračunska tablica Excel táblázat Folio de calculo Excel Lembar sebar Excel Foglio di calcolo Excel Excel スプレッドシート Excel-ის ცხრილი Excel электрондық кестесі Excel 스프레드시트 Excel skaičialentė Excel izklājlapa Excel regneark Excel-rekenblad Excel-rekneark fuèlh de calcul Excel Arkusz Excel folha de cálculo Excel Planilha do Excel Foaie de calcul Excel электронная таблица Excel Zošit Excel Razpredelnica Microsoft Excel Fletë llogaritje Excel Екселова табела Excel-kalkylblad Excel çalışma sayfası ел. таблиця Excel Bảng tính Excel Microsoft Excel 工作簿 Excel 試算表 Excel add-in Приставка — Excel complement d'Excel doplněk aplikace Excel Excel-tilføjelse Excel Add-in Πρόσθετο Excel Excel add-in complemento de Excel Excel gehigarria Excel-lisäosa complément Excel complemento de Excel תוסף של Excel Excel priključak Excel bővítmény Add-in Excel Add-in Excel Add-in Excel Excel アドイン Excel-ის დამატება Excel қосымшасы Excel 추가 기능 Excel pievienojumprogramma Excel add-in complement Excel Dodatek Excel Extensão Excel Suplemento do Excel дополнение Excel Doplnok aplikácie Excel Vstavek Excel Екселов додатак Excel-tillägg Excel eklentisi додаток Excel Excel 附加组件 Excel 增益集 Excel 2007 binary spreadsheet Таблица — Excel 2007, двоична full de càlcul binari d'Excel 2007 binární formát sešitu Excel 2007 Binært Excel 2007-regneark Excel-2007-Tabelle (binär) Δυαδικό λογιστικό φύλλο Excel 2007 Excel 2007 binary spreadsheet hoja de cálculo binaria de Excel 2007 Excel 2007 kalkulu-orri binarioa Excel 2007:n binaarinen taulukko feuille de calcul binaire Excel 2007 ficheiro binario de folla de cálculo Excel 2007 גיליון נתונים בינרי של Excel 2007 Excel 2007 binarna proračunska tablica Excel 2007 bináris táblázat Folio de calculo binari Excel 2007 Lembar kerja biner Excel 2007 Foglio di calcolo binario Excel 2007 Excel 2007 バイナリスプレッドシート Excel 2007-ის ბინარული ცხრილი Excel 2007 бинарды кестесі Excel 2007 바이너리 스프레드시트 Excel 2007 binārā izklājlapa Excel 2007 binary spreadsheet fuèlh de calcul binaire Excel 2007 Binarny arkusz Excel 2007 folha de cálculo binária Excel 2007 Planilha binária do Excel 2007 двоичная электронная таблица Excel 2007 Binárny zošit Excel 2007 Binarna preglednica Excel 2007 Ексел 2007 бинарна табела Binärt Excel 2007-kalkylblad Excel 2007 ikilik çalışma sayfası бінарна електронна таблиця Excel 2007 Excel 2007 二进制工作表 Excel 2007 二進位試算表 Excel spreadsheet جدول Excel Raźlikovy akruš Excel Таблица — Excel full de càlcul d'Excel sešit Excel Excelregneark Excel-Tabelle Λογιστικό φύλλο Excel Excel spreadsheet Excel-kalkultabelo hoja de cálculo de Excel Excel kalkulu-orria Excel-taulukko Excel rokniark feuille de calcul Excel scarbhileog Excel folla de cálculo de Excel גליון נתונים של אקסל Excel proračunska tablica Excel táblázat Folio de calculo Excel Lembar sebar Excel Foglio di calcolo Excel Excel スプレッドシート Excel-ის ცხრილი Excel электрондық кестесі Excel 스프레드시트 Excel skaičialentė Excel izklājlapa Excel regneark Excel-rekenblad Excel-rekneark fuèlh de calcul Excel Arkusz Excel folha de cálculo Excel Planilha do Excel Foaie de calcul Excel электронная таблица Excel Zošit Excel Razpredelnica Microsoft Excel Fletë llogaritje Excel Екселова табела Excel-kalkylblad Excel çalışma sayfası ел. таблиця Excel Bảng tính Excel Microsoft Excel 工作簿 Excel 試算表 Excel spreadsheet template plantilla de full de càlcul d'Excel šablona tabulky Excel Excel-regnearksskabelon Excel-Tabellenvorlage Πρότυπο λογιστικού φύλλου Excel Excel spreadsheet template plantilla de libro de Excel Excel kalkulu-orri txantiloia Excel-taulukkomalli Predložak Excel proračunske tablice Excel munkafüzetsablon Patrono de folio de calculo Excel Templat lembar kerja Excel Modello foglio di calcolo Excel Excel кестесінің үлгісі 액셀 스프레드시트 양식 Modèl de fuèlh de calcul Excel Szablon arkusza Excel modelo de folha de cálculo Excel Modelo de planilha do Excel шаблон таблицы Excel Šablóna tabuľky aplikácie Excel Шаблон табеле Ексела Excel-kalkylarksmall Excel hesap tablosu şablonu шаблон електронної таблиці Excel Microsoft Excel 工作簿模板 Excel 試算表範本 PowerPoint presentation عرض تقديمي PowerPoint Prezentacyja PowerPoint Презентация — PowerPoint presentació de PowerPoint prezentace PowerPoint PowerPoint-præsentation PowerPoint-Präsentation Παρουσίαση PowerPoint PowerPoint presentation PowerPoint-prezentaĵo presentación de PowerPoint PowerPoint aurkezpena PowerPoint-esitys PowerPoint framløga présentation PowerPoint láithreoireacht PowerPoint presentación de PowerPoint מצגת PowerPoint PowerPoint prezentacija PowerPoint prezentáció Presentation PowerPoint Presentasi PowerPoint Presentazione PowerPoint PowerPoint プレゼンテーション PowerPoint презентациясы PowerPoint 프레젠테이션 PowerPoint pateiktis PowerPoint prezentācija PowerPoint-presentasjon PowerPoint-presentatie PowerPoint-presentasjon presentacion PowerPoint Prezentacja PowerPoint apresentação PowerPoint Apresentação do PowerPoint Prezentare PowerPoint презентация PowerPoint Prezentácia PowerPoint Predstavitev Microsoft PowerPoint Prezantim PowerPoint Пауер Поинт презентација PowerPoint-presentation PowerPoint sunumu презентація PowerPoint Trình diễn PowerPoint Microsoft PowerPoint 演示文稿 PowerPoint 簡報 PowerPoint add-in Приставка — PowerPoint complement de PowerPoint doplněk PowerPoint PowerPoint-tilføjelse PowerPoint Add-in Πρόσθετο PowerPoint PowerPoint add-in complemento de PowerPoint PowerPoint gehigarria PowerPoint-lisäosa complément PowerPoint complemento de PowerPoint תוסף של PowerPoint PowerPoint priključak PowerPoint bővítmény Add-in PowerPoint Add-in PowerPoint Add-in PowerPoint PowerPoint アドイン PowerPoint-ის დამატება PowerPoint қосымшасы PowerPoint 추가 기능 PowerPoint pievienojumprogramma PowerPoint add-in complement PowerPoint Dodatek PowerPoint extensão PowerPoint Suplemento do PowerPoint дополнение PowerPoint Doplnok aplikácie PowerPoint Vstavek PowerPoint Пауер Поинт додатак PowerPoint-tillägg PowerPoint eklentisi додаток PowerPoint PowerPoint 附加组件 PowerPoint 增益集 PowerPoint presentation عرض تقديمي PowerPoint Prezentacyja PowerPoint Презентация — PowerPoint presentació de PowerPoint prezentace PowerPoint PowerPoint-præsentation PowerPoint-Präsentation Παρουσίαση PowerPoint PowerPoint presentation PowerPoint-prezentaĵo presentación de PowerPoint PowerPoint aurkezpena PowerPoint-esitys PowerPoint framløga présentation PowerPoint láithreoireacht PowerPoint presentación de PowerPoint מצגת PowerPoint PowerPoint prezentacija PowerPoint prezentáció Presentation PowerPoint Presentasi PowerPoint Presentazione PowerPoint PowerPoint プレゼンテーション PowerPoint презентациясы PowerPoint 프레젠테이션 PowerPoint pateiktis PowerPoint prezentācija PowerPoint-presentasjon PowerPoint-presentatie PowerPoint-presentasjon presentacion PowerPoint Prezentacja PowerPoint apresentação PowerPoint Apresentação do PowerPoint Prezentare PowerPoint презентация PowerPoint Prezentácia PowerPoint Predstavitev Microsoft PowerPoint Prezantim PowerPoint Пауер Поинт презентација PowerPoint-presentation PowerPoint sunumu презентація PowerPoint Trình diễn PowerPoint Microsoft PowerPoint 演示文稿 PowerPoint 簡報 PowerPoint slide dispositiva de PowerPoint promítání PowerPoint PowerPoint-dias PowerPoint-Folie Διαφάνεια PowerPoint PowerPoint slide diapositiva de PowerPoint PowerPoint diapositiba PowerPoint-dia PowerPoint prezentacija PowerPoint dia Diapositiva PowerPoint Salindia PowerPoint Diapositiva PowerPoint PowerPoint слайды 파워포인트 슬라이드 Diapositiva PowerPoint Slajd PowerPoint diapositivo PowerPoint Slide do PowerPoint слайд PowerPoint Snímka aplikácie PowerPoint Слајд Пауер Поинта PowerPoint-bildspel PowerPoint sunusu слайд PowerPoint PowerPoint 文稿 PowerPoint 投影片 PowerPoint presentation عرض تقديمي PowerPoint Prezentacyja PowerPoint Презентация — PowerPoint presentació de PowerPoint prezentace PowerPoint PowerPoint-præsentation PowerPoint-Präsentation Παρουσίαση PowerPoint PowerPoint presentation PowerPoint-prezentaĵo presentación de PowerPoint PowerPoint aurkezpena PowerPoint-esitys PowerPoint framløga présentation PowerPoint láithreoireacht PowerPoint presentación de PowerPoint מצגת PowerPoint PowerPoint prezentacija PowerPoint prezentáció Presentation PowerPoint Presentasi PowerPoint Presentazione PowerPoint PowerPoint プレゼンテーション PowerPoint презентациясы PowerPoint 프레젠테이션 PowerPoint pateiktis PowerPoint prezentācija PowerPoint-presentasjon PowerPoint-presentatie PowerPoint-presentasjon presentacion PowerPoint Prezentacja PowerPoint apresentação PowerPoint Apresentação do PowerPoint Prezentare PowerPoint презентация PowerPoint Prezentácia PowerPoint Predstavitev Microsoft PowerPoint Prezantim PowerPoint Пауер Поинт презентација PowerPoint-presentation PowerPoint sunumu презентація PowerPoint Trình diễn PowerPoint Microsoft PowerPoint 演示文稿 PowerPoint 簡報 PowerPoint presentation template plantilla de presentació de PowerPoint šablona prezentace PowerPoint PowerPoint-præsentationsskabelon PowerPoint-Präsentationsvorlage Πρότυπο παρουσίασης PowerPoint PowerPoint presentation template plantilla de presentación de PowerPoint PowerPoint aurkezpen txantiloia PowerPoint-esitysmalli Predložak PowerPoint prezentacije PowerPoint bemutatósablon Patrono de presentation PowerPoint Templat presentasi PowerPoint Modello presentazione PowerPoint PowerPoint презентация үлгісі 파워포인드 프리젠테이션 양식 Modèl de presentacion PowerPoint Szablon prezentacji PowerPoint modelo de apresentação PowerPoint Modelo de apresentação do PowerPoint шаблон презентации PowerPoint Šablóna prezentácie aplikácie PowerPoint Шаблон презентације Пауер Поинта PowerPoint-presentationsmall PowerPoint sunum şablonu шаблон презентації PowerPoint Microsoft PowerPoint 演示文稿模板 PowerPoint 簡報範本 Office Open XML Visio Drawing dibuix en Office Open XML de Visio kresba Office Open XML Visio Office Open XML Visio-tegning Office-Open-XML-Visio-Zeichnung Σχέδιο Office Open XML Visio Office Open XML Visio Drawing dibujo en OOXML de Visio Office Open XML Visio marrazkia Office Open XML Visio -piirros Office Open XML Visio crtež Office Open XML Visio rajz Designo Office Open XML Visio Gambar Visio Office Open XML Disegno Visio Office Open XML Office Open XML Visio суреті 오피스 오픈 XML 비지오 드로잉 Rysunek Office Open XML Visio desenho Office Open XML Visio Desenho do Visio em Office Open XML схема Visio формата Office Open XML Kresba aplikácie Visio Office Open XML Офис опен ИксМЛ Визио цртање Office Open XML Visio-teckning Office Open XML Visio Çizimi схема VIisio у форматі Office Open XML OOXML Visio 绘图 Office Open XML Visio 繪圖 Office Open XML Visio Template plantilla en Office Open XML de Visio šablona Office Open XML Visio Office Open XML Visio-skabelon Office-Open-XML-Visio-Vorlage Πρότυπο Office Open XML Visio Office Open XML Visio Template plantilla en OOXML de Visio Office Open XML Visio txantiloia Office Open XML Visio -malli Predložak Office Open XML Visio Office Open XML Visio sablon Patrono Office Open XML Visio Templat Visio Office Open XML Modello Visio Office Open XML Office Open XML Visio үлгісі 오피스 오픈 XML 비지오 양식 Szablon Office Open XML Visio modelo Office Open XML Visio Modelo do Visio em Office Open XML шаблон Visio формата Office Open XML Šablóna aplikácie Visio Office Open XML Офис опен ИксМЛ Визио шаблон Office Open XML Visio-mall Office Open XML Visio Şablonu шаблон Visio у форматі Office Open XML OOXML Visio 模板 Office Open XML Visio 範本 Office Open XML Visio Stencil patró en Office Open XML de Visio objekty Office Open XML Visio Office Open XML Visio-stencil Office-Open-XML-Visio-Schablone Office Open XML Visio Stencil esténcil en OOXML de Visio Office Open XML Visio šablona Office Open XML Visio stencil Stencil Office Open XML Visio Stensil Visio Office Open XML Stencil Visio Office Open XML 오피스 오픈 XML 비지오 스텐실 Wzór Office Open XML Visio Stencil Office Open XML Visio Estêncil do Visio em Office Open XML трафарет Visio формата Office Open XML Objekt aplikácie Visio Office Open XML Офис опен ИксМЛ Визио шаблон Office Open XML Visio-stencil Office Open XML Visio Kalıbı трафарет Visio у форматі Office Open XML OOXML Visio 模具 Office Open XML Visio 圖形樣本 Office Open XML Visio Drawing dibuix en Office Open XML de Visio kresba Office Open XML Visio Office Open XML Visio-tegning Office-Open-XML-Visio-Zeichnung Σχέδιο Office Open XML Visio Office Open XML Visio Drawing dibujo en OOXML de Visio Office Open XML Visio marrazkia Office Open XML Visio -piirros Office Open XML Visio crtež Office Open XML Visio rajz Designo Office Open XML Visio Gambar Visio Office Open XML Disegno Visio Office Open XML Office Open XML Visio суреті 오피스 오픈 XML 비지오 드로잉 Rysunek Office Open XML Visio desenho Office Open XML Visio Desenho do Visio em Office Open XML схема Visio формата Office Open XML Kresba aplikácie Visio Office Open XML Офис опен ИксМЛ Визио цртање Office Open XML Visio-teckning Office Open XML Visio Çizimi схема VIisio у форматі Office Open XML OOXML Visio 绘图 Office Open XML Visio 繪圖 Office Open XML Visio Template plantilla en Office Open XML de Visio šablona Office Open XML Visio Office Open XML Visio-skabelon Office-Open-XML-Visio-Vorlage Πρότυπο Office Open XML Visio Office Open XML Visio Template plantilla en OOXML de Visio Office Open XML Visio txantiloia Office Open XML Visio -malli Predložak Office Open XML Visio Office Open XML Visio sablon Patrono Office Open XML Visio Templat Visio Office Open XML Modello Visio Office Open XML Office Open XML Visio үлгісі 오피스 오픈 XML 비지오 양식 Szablon Office Open XML Visio modelo Office Open XML Visio Modelo do Visio em Office Open XML шаблон Visio формата Office Open XML Šablóna aplikácie Visio Office Open XML Офис опен ИксМЛ Визио шаблон Office Open XML Visio-mall Office Open XML Visio Şablonu шаблон Visio у форматі Office Open XML OOXML Visio 模板 Office Open XML Visio 範本 Office Open XML Visio Stencil patró en Office Open XML de Visio objekty Office Open XML Visio Office Open XML Visio-stencil Office-Open-XML-Visio-Schablone Office Open XML Visio Stencil esténcil en OOXML de Visio Office Open XML Visio šablona Office Open XML Visio stencil Stencil Office Open XML Visio Stensil Visio Office Open XML Stencil Visio Office Open XML 오피스 오픈 XML 비지오 스텐실 Wzór Office Open XML Visio Stencil Office Open XML Visio Estêncil do Visio em Office Open XML трафарет Visio формата Office Open XML Objekt aplikácie Visio Office Open XML Офис опен ИксМЛ Визио шаблон Office Open XML Visio-stencil Office Open XML Visio Kalıbı трафарет Visio у форматі Office Open XML OOXML Visio 模具 Office Open XML Visio 圖形樣本 Word document مستند Word Dakument Word Документ — Word document Word dokument Word Worddokument Word-Dokument Έγγραφο Word Word document Word-dokumento documento de Word Word dokumentua Word-asiakirja Word skjal document Word cáipéis Word documento de Word מסמך Word Word dokument Word dokumentum Documento Word Dokumen Word Documento Word Word ドキュメント Word құжаты Word 문서 Word dokumentas Word dokuments Word-dokument Word-document Word-dokument document Word Dokument Word documento Word Documento do Word Document Word документ Word Dokument Word Dokument Word Dokument Word Вордов документ Word-dokument Word belgesi документ Word Tài liệu Word Microsoft Word 文档 Word 文件 Word document template plantilla de document Word šablona dokumentu Word Word-dokumentskabelon Word-Dokumentvorlage Πρότυπο έγγραφο Word Word document template plantilla de documento de Word Word dokumentuaren txantiloia Word-asiakirjamalli Predložak Word dokumenta Word dokumentumsablon Patrono de documento Word Templat dokumen Word Modello documento Word Word құжатының үлгісі 워드 문서 양식 modèl de document Word Szablon dokumentu Word modelo de documento Word Modelo de documento do Word шаблон документа Word Šablóna dokumentu aplikácie Word Шаблон документа Ворда Word-dokumentmall Word belgesi şablonu шаблон документа Word Microsoft Word 文档模板 Word 文件範本 XPS document مستند XPS Dakument XPS Документ — XPS document XPS dokument XPS XPS-dokument XPS-Dokument Έγγραφο XPS XPS document XPS-dokumento documento XPS XPS dokumentua XPS-asiakirja XPS skjal document XPS cáipéis XPS documento XPS מסמך XPS XPS dokument XPS dokumentum Documento XPS Dokumen XPS Documento XPS XPS ドキュメント XPS құжаты XPS 문서 XPS dokumentas XPS dokuments XPS-dokument XPS-document XPS-dokument document XPS Dokument XPS documento XPS Documento XPS Document XPS документ XPS Dokument XPS Dokument XPS Dokument XPS ИксПС документ XPS-dokument XPS belgesi документ XPS Tài liệu XPS XPS 文档 XPS 文件 XPS Open XML Paper Specification Microsoft Works document مستند Microsoft Works Dakument Microsoft Works Документ — Microsoft Works document de Microsoft Works dokument Microsoft Works Microsoft Works-dokument Microsoft-Works-Dokument Έγγραφο Microsoft Works Microsoft Works document documento de Microsoft Works Microsoft Works dokumentua Microsoft Works -asiakirja Microsoft Works skjal document Microsoft Works cáipéis Microsoft Works documento de Microsoft Works מסמך Microsoft Works Microsoft Works dokument Microsoft Works dokumentum Documento Microsoft Works Dokumen Microsoft Works Documento Microsoft Works Microsoft Works ドキュメント Microsoft Works-ის დოკუმენტი Microsoft Works құжаты Microsoft Works 문서 Microsoft Works dokumentas Microsoft Works dokuments Microsoft Works-dokument Microsoft Works-document Microsoft Works-dokument document Microsoft Works Dokument Microsoft Works documento Microsoft Works Documento do Microsoft Works Document Microsoft Works документ Microsoft Works Dokument Microsoft Works Dokument Microsoft Works Dokument Microsoft Works Микрософтов Воркс документ Microsoft Works-dokument Microsoft Works belgesi документ Microsoft Works Tài liệu Microsoft Works Microsoft Works 文档 微軟 Works 文件 Microsoft Visio document Документ — Microsoft Visio document de Microsoft Visio dokument Microsoft Visio Microsoft Visio-dokument Microsoft-Visio-Dokument Έγγραφο Microsoft Visio Microsoft Visio document documento de Microsoft Visio Microsoft Visio dokumentua Microsoft Visio -asiakirja document Microsoft Visio Documento de Microsoft Visio מסמך Microsoft Visio dokument Microsoft Visio dokumentum Documento Microsoft Visio Dokumen Microsoft Visio Documento Microsoft Visio Microsoft Visio ドキュメント Microsoft Visio-ის დოკუმენტი Microsoft Visio құжаты Microsoft Visio 문서 Microsoft Visio dokuments Microsoft Visio document document Microsoft Visio Dokument Microsoft Visio documento Microsoft Visio Documento do Microsoft Visio документ Microsoft Visio Dokument Microsoft Visio Dokument Microsoft Visio Микрософтов Визио документ Microsoft Visio-dokument Microsoft Visio belgesi документ Microsoft Visio Microsoft Visio 文档 Microsoft Visio文件 Word document مستند Word Dakument Word Документ — Word document Word dokument Word Worddokument Word-Dokument Έγγραφο Word Word document Word-dokumento documento de Word Word dokumentua Word-asiakirja Word skjal document Word cáipéis Word documento de Word מסמך Word Word dokument Word dokumentum Documento Word Dokumen Word Documento Word Word ドキュメント Word құжаты Word 문서 Word dokumentas Word dokuments Word-dokument Word-document Word-dokument document Word Dokument Word documento Word Documento do Word Document Word документ Word Dokument Word Dokument Word Dokument Word Вордов документ Word-dokument Word belgesi документ Word Tài liệu Word Microsoft Word 文档 Word 文件 Word template قالب Word Šablon Word Шаблон за документи — Word plantilla de Word šablona Word Wordskabelon Word-Vorlage Πρότυπο έγγραφο Word Word template Word-ŝablono plantilla de Word Word txantiloia Word-malli Word formur modèle Word teimpléad Word Plantilla de Word תבנית Word Word predložak Word sablon Patrono Word Templat Word Modello Word Word テンプレート Word үлгісі Word 서식 Word šablonas Word veidne Word-mal Word-sjabloon Word-mal modèl Word Szablon Word modelo Word Modelo do Word Șablon Word шаблон Word Šablóna Word Predloga dokumenta Microsoft Word Model Word Вордов шаблон Word-mall Word şablonu шаблон Word Mẫu Word Word 模板 Word 範本 GML document document GML dokument GML GML-dokument GML-Dokument Έγγραφο GML GML document documento GML GML dokumentua GML-asiakirja document GML Documento GML מסמך GML GML dokument GML dokumentum Documento GML Dokumen GML Documento GML GML ドキュメント GML құжаты GML 문서 GML dokuments document GML Dokument GML documento GML Documento GML документ GML Dokument GML Dokument GML ГМЛ документ GML-dokument GML belgesi документ GML GML 文档 GML 文件 GML Geography Markup Language GNUnet search file ملف بحث GNUnet fajł pošuku GNUnet Указател за търсене — GNUnet fitxer de cerca GNUnet vyhledávací soubor GNUnet GNunet-søgefil GNUnet-Suchdatei Αρχείο αναζήτησης GNUnet GNUnet search file archivo de búsqueda GNUnet GNUnet bilaketako fitxategia GNUnet-hakutiedosto GNUnet leitifíla fichier de recherche GNUnet comhad cuardaigh GNUnet ficheiro de busca de GNUnet קובץ חיפוש של GNUnet GNUnet datoteka pretrage GNUnet keresési fájl File de recerca GNUnet Berkas telusur GNUnet File ricerca GNUnet GNUnet 検索ファイル GNUnet ძებნის ფაილი GNUnet іздеу файлы GNUnet 검색 파일 GNUnet paieškos failas GNUnet meklēšanas datne GNUnet søkefil GNUnet-zoekbestand GNUnet-søkjefil fichièr de recèrca GNUnet Plik wyszukiwania GNUnet ficheiro de procura GNUnet Arquivo de pesquisa do GNUnet Fișier căutare GNUnet файл поиска GNUnet Vyhľadávací súbor GNUnet Iskalna datoteka GNUnet File kërkimi GNUnet ГНУнет датотека претраге GNUnet-sökfil GNUnet arama dosyası файл пошуку GNUnet Tập tin tìm kiếm GNUnet GNUnet 搜索文件 GNUnet 搜尋檔案 TNEF message رسالة TNEF List TNEF Съобщение — TNEF missatge TNEF zpráva TNEF TNEF-meddelelse TNEF-Nachricht Μήνυμα TNEF TNEF message mensaje TNEF TNEF mezua TNEF-viesti TNEF boð message TNEF teachtaireacht TNEF mensaxe TNEF הודעת TNEF TNEF poruka TNEF üzenet Message TNEF Pesan TNEF Messaggio TNEF TNEF メッセージ TNEF мәлімдемесі TNEF 메시지 TNEF žinutė TNEF ziņojums TNEF-melding TNEF-bericht TNEF-melding messatge TNEF Wiadomość TNEF mensagem TNEF Mensagem TNEF Mesaj TNEF сообщение TNEF Správa TNEF Datoteka sporočila TNEF Mesazh TNEF ТНЕФ порука TNEF-meddelande TNEF iletisi повідомлення TNEF Thông điệp TNEF TNEF 信件 TNEF 訊息 TNEF Transport Neutral Encapsulation Format StarCalc spreadsheet جدول StarCalc StarCalc hesab cədvəli Raźlikovy arkuš StarCalc Таблица — StarCalc full de càlcul de StarCalc sešit StarCalc Taenlen StarCalc StarCalc-regneark StarCalc-Tabelle Λογιστικό φύλλο StarCalc StarCalc spreadsheet StarCalc-kalkultabelo hoja de cálculo de StarCalc StarCalc kalkulu-orria StarCalc-taulukko StarCalc rokniark feuille de calcul StarCalc scarbhileog StarCalc folla de cálculo de StarCalc גליון נתונים של StarCalc StarCalc proračunska tablica StarCalc-munkafüzet Folio de calculo StarCalc Lembar sebar StarCalc Foglio di calcolo StarCalc StarCalc スプレッドシート StarCalc электрондық кестесі StarCalc 스프레드시트 StarCalc skaičialentė StarCalc izklājlapa Hamparan StarCalc StarCalc-regneark StarCalc-rekenblad StarCalc-rekneark fuèlh de calcul StarCalc Arkusz StarCalc folha de cálculo do StarCalc Planilha do StarCalc Foaie de calcul StarCalc электронная таблица StarCalc Zošit StarCalc Preglednica StarCalc Fletë llogaritjesh StarCalc Табела Стар Рачуна StarCalc-kalkylblad StarCalc çalışma sayfası ел. таблиця StarCalc Bảng tính StarCalc STarCalc 工作簿 StarCalc 試算表 StarChart chart مخطط StarChart StarChart cədvəli Dyjahrama StarChart Диаграма — StarChart diagrama de StarChart graf StarChart Siart StarChart StarChart-diagram StarChart-Diagramm Γράφημα StarChart StarChart chart StarChart-diagramo gráfico de StarChart StarChart diagrama StarChart-kaavio StarChart strikumynd graphique StarChart cairt StarChart gráfica de StarChart טבלה של StarChart StarChart grafikon StarChart-grafikon Graphico StarChart Bagan StarChart Grafico StarChart StarChart チャート StarChart диаграммасы StarCalc 표 StarChart diagrama StarChart diagramma Carta StarChart StarChart graf StarChart-kaart StarChart-graf grafic StarChart Wykres StarChart gráfico do StarChart Gráfico do StarChart Diagramă StarChart диаграмма StarChart Graf StarChart Datoteka grafikona StarChart Grafik StarChart График Стар Графика StarChart-diagram StarChart çizgelgesi діаграма StarChart Đồ thị StarChart STarChart 图表 StarChart 圖表 StarDraw drawing تصميم StarDraw StarDraw çəkimi Rysunak StarDraw Чертеж — StarDraw dibuix de StarDraw kresba StarDraw Darlun StarDraw StarDraw-tegning StarDraw-Zeichnung Σχέδιο StarDraw StarDraw drawing StarDraw-grafikaĵo dibujo de StarDraw StarDraw marrazkia StarDraw-piirros StarDraw tekning dessin StarDraw líníocht StarDraw debuxo de StarDraw ציור של StarDrawing StarDraw crtež StarDraw-rajz Designo StarDraw Gambar StarDraw Disegno StarDraw StarDraw ドロー StarDraw суреті StarCalc 드로잉 StarDraw piešinys StarDraw zīmējums Lukisan StarDraw StarDraw tegning StarDraw-tekening StarDraw-teikning dessenh StarDraw Rysunek StarDraw desenho do StarDraw Desenho do StarDraw Desen StarDraw изображение StarDraw Kresba StarDraw Datoteka risbe StarDraw Vizatim StarDraw Цртеж Стар Цртежа StarDraw-teckning StarDraw çizimi малюнок StarDraw Bản vẽ StarDraw STarDraw 绘图 StarDraw 繪圖 StarImpress presentation عرض تقديمي StarImpress StarImpress təqdimatı Prezentacyja StarImpress Презентация — StarImpress presentació de StarImpress prezentace StarImpress Cyflwyniad StarImpress StarImpress-præsentation StarImpress-Präsentation Παρουσίαση StarImpress StarImpress presentation StarImpress-prezentaĵo presentación de StarImpress StarImpress aurkezpena StarImpress-esitys StarImpress framløga présentation StarImpress láithreoireacht StarImpress presentación de StarImpress מצגת של StarImpress StarImpress prezentacija StarImpress-bemutató Presentation StarImpress Presentasi StarImpress Presentazione StarImpress StarImpress プレゼンテーション StarImpress презентациясы StarImpress 프레젠테이션 StarImpress pateiktis StarImpress prezentācija Persembahan StarImpress StarImpress-presentasjon StarImpress-presentatie StarImpress-presentasjon presentacion StarImpress Prezentacja StarImpress apresentação do StarImpress Apresentação do StarImpress Prezentare StarImpress презентация StarImpress Prezentácia StarImpress Predstavitev StarImpress Prezantim StarImpress Презентација Стар Импреса StarImpress-presentation StarImpress sunumu презентація StarImpress Trình diễn StarImpress STarImpress 演示文稿 StarImpress 簡報檔 StarMail email بريد StarMail الإلكتروني Email StarMail Електронно писмо — StarMail correu electrònic de StarMail e-mail StarMail StarMail-e-brev StarMail-E-Mail Ηλ. μήνυμα StarMail StarMail email StarMail-retpoŝto correo electrónico de StarMail StarMail helb.el. StarMail-sähköposti StarMail t-postur courriel StarMail ríomhphost StarMail Correo electrónico de StarMail דוא״ל של StarMail StarMail e-pošta StarMail e-mail Message electronic StarMail Email StarMail Email StarMail StarMail メール StarMail электрондық хаты StarMail 전자우편 StarMail el. laiškas StarMail epasts Emel StarMail StarMail-melding StarMail-e-mail StarMail-fil corrièr electronic StarMail E-Mail StarMail email do StarMail E-mail do StarMail Email StarEmail электронное письмо StarMail E-mail StarMail Datoteka pošte StarMail Mesazh StarMail Ел. пошта Стар Поште StarMail-e-post StarMail epostası поштове повідомлення StarMail Thư điện tử StarMail STarMail 电子邮件 StarMail 郵件 StarMath formula صيغة StarMath Formuła StarMath Формула — StarMath fórmula de StarMath vzorec StarMath StarMath-formel StarMath-Formel Μαθηματικός τύπος StarMath StarMath formula StarMath-formulo fórmula de StarMath StarMath formula StarMath-kaava StarMath frymil formule StarMath foirmle StarMath fórmula de StarMath נוסחה של StarMath StarMath formula StarMath-képlet Formula StarMath Formula StarMath Formula StarMath StarMath 計算式 StarMath формуласы StarMath 수식 StarMath formulė StarMath formula Formula StarMath StarMath-formel StarMath-formule StarMath-formel formula StarMath Formuła StarMath fórmula do StarMath Fórmula do StarMath Formulă StarMath формула StarMath Vzorec StarMath Datoteka formule StarMath Formulë StarMath Формула Стар Математике StarMath-formel StarMath formülü формула StarMath Công thức StarMath STarMath 公式 StarMath 公式 StarWriter document مستند StarWriter StarWriter sənədi Dakument StarWriter Документ — StarWriter document StarWriter dokument StarWriter Dogfen StarWriter StarWriter-dokument StarWriter-Dokument Έγγραφο StarWriter StarWriter document StarWriter-dokumento documento de StarWriter StarWriter dokumentua StarWriter-asiakirja StarWriter skjal document StarWriter cáipéis StarWriter documento de StarWriter מסמך של StarWriter StarWriter dokument StarWriter-dokumentum Documento StarWriter Dokumen StarWriter Documento StrarWriter StarWriter ドキュメント StarWriter құжаты StarWriter 문서 StarWriter dokumentas StarWriter dokuments Dokumen StarWriter StarWriter-dokument StarWriter-document StarWriter document document StarWriter Dokument StarWriter documento do StarWriter Documento do StarWriter Document StarWriter документ StarWriter Dokument StarWriter Dokument StarWriter Dokument StarWriter Документ Стар Писца StarWriter-dokument StarWriter belgesi документ StarWriter Tài liệu StarWriter STarWriter 文档 StarWriter 文件 OpenOffice Calc spreadsheet جدول Calc المكتب المفتوح Raźlikovy arkuš OpenOffice Calc Таблица — OpenOffice Calc full de càlcul d'OpenOffice Calc sešit OpenOffice Calc OpenOffice Calc-regneark OpenOffice-Calc-Tabelle Λογιστικό φύλλο OpenOffice Calc OpenOffice Calc spreadsheet hoja de cálculo de OpenOffice Calc OpenOffice.org Calc kalkulu-orria OpenOffice Calc -taulukko OpenOffice Calc rokniark feuille de calcul OpenOffice Calc scarbhileog OpenOffice Calc folla de cálculo de OpenOffice Calc גליון נתונים של OpenOffice Calc OpenOffice Calc proračunska tablica OpenOffice Calc táblázat Folio de calculo OpenOffice Calc Lembar sebar OpenOffice Calc Foglio di calcolo OpenOffice Calc OpenOffice Calc スプレッドシート OpenOffice Calc-ის ცხრილი OpenOffice Calc электрондық кестесі OpenOffice Calc 스프레드시트 OpenOffice Calc skaičialentė OpenOffice Calc izklājlapa OpenOffice Calc-regneark OpenOffice.org Calc-rekenblad OpenOffice Calc-rekneark fuèlh de calcul OpenOffice Calc Arkusz kalkulacyjny OpenOffice.org Calc folha de cálculo OpenOffice Calc Planilha do OpenOffice Calc Foaie de calcul OpenOffice Calc электронная таблица OpenOffice Calc Zošit OpenOffice Calc Razpredelnica OpenOffice.org Calc Fletë llogaritjesh OpenOffice Calc Табела Опен Офис Рачуна OpenOffice Calc-kalkylblad OpenOffice Calc çalışma sayfası ел. таблиця OpenOffice Calc Bảng tính Calc của OpenOffice.org OpenOffice.org Calc 工作簿 OpenOffice Calc 試算表 OpenOffice Calc template قالب Calc المكتب المفتوح Šablon OpenOffice Calc Шаблон за таблици — OpenOffice Calc plantilla d'OpenOffice Calc šablona OpenOffice Calc OpenOffice Calc-skabelon OpenOffice-Calc-Vorlage Πρότυπο OpenOffice Calc OpenOffice Calc template plantilla de OpenOffice Calc OpenOffice Calc txantiloia OpenOffice Calc -malli OpenOffice Calc formur modèle OpenOffice Calc teimpléad OpenOffice Calc modelo de OpenOffice Calc תבנית של OpenOffice Calc OpenOffice Calc predložak OpenOffice Calc sablon Patrono OpenOffice Calc Templat OpenOffice Calc Modello OpenOffice Calc OpenOffice Calc テンプレート OpenOffice Calc-ის შაბლონი OpenOffice Calc үлгісі OpenOffice Calc 스프레드시트 문서 서식 OpenOffice Calc šablonas OpenOffice Calc veidne OpenOffice Calc-mal OpenOffice.org Calc-sjabloon OpenOffice Calc-mal modèl OpenOffice Calc Szablon arkusza OpenOffice.org Calc modelo OpenOffice Calc Modelo do OpenOffice Calc Șablon OpenOffice Calc шаблон OpenOffice Calc Šablóna OpenOffice Calc Predloga OpenOffice.org Calc Model OpenOffice Calc Шаблон Опен Офис Рачуна OpenOffice Calc-mall OpenOffice Calc şablonu шаблон ел.таблиці OpenOffice Calc Mẫu bảng tính Calc của OpenOffice.org OpenOffice.org Calc 工作簿模板 OpenOffice Calc 範本 OpenOffice Draw drawing تصميم Draw المكتب المفتوح Rysunak OpenOffice Draw Чертеж — OpenOffice Draw dibuix d'OpenOffice Draw kresba OpenOffice Draw OpenOffice Draw-tegning OpenOffice-Draw-Zeichnung Σχέδιο OpenOffice Draw OpenOffice Draw drawing dibujo de OpenOffice Draw OpenOffice.org Draw marrazkia OpenOffice Draw -piirros OpenOffice Draw tekning dessin OpenOffice Draw líníocht OpenOffice Draw debuxo de OpenOffice Draw ציור של OpenOffice Draw OpenOffice Draw crtež OpenOffice Draw rajz Designo OpenOffice Draw Gambar OpenOffice Draw Disegno OpenOffice Draw OpenOffice Draw ドロー OpenOffice Draw-ის ნახაზი OpenOffice Draw суреті OpenOffice Draw 그림 OpenOffice Draw piešinys OpenOffice Draw zīmējums OpenOffice Draw-tegning OpenOffice.org Draw-tekening OpenOffice Draw-teikning dessenh OpenOffice Draw Rysunek OpenOffice.org Draw desenho OpenOffice Draw Desenho do OpenOffice Draw Desen OpenOffice Draw изображение OpenOffice Draw Kresba OpenOffice Draw Datoteka risbe OpenOffice.org Draw Vizatim OpenOffice Draw Цртеж Опен Офис Цртежа OpenOffice Draw-teckning OpenOffice Draw çizimi малюнок OpenOffice Draw Bản vẽ Draw của OpenOffice.org OpenOffice.org Draw 绘图 OpenOffice Draw 繪圖 OpenOffice Draw template قالب Draw المكتب المفتوح Šablon OpenOffice Draw Шаблон за чертежи — OpenOffice Draw plantilla d'OpenOffice Draw šablona OpenOffice Draw OpenOffice Draw-skabelon OpenOffice-Draw-Vorlage Πρότυπο OpenOffice Draw OpenOffice Draw template plantilla de OpenOffice Draw OpenOffice Draw txantiloia OpenOffice Draw -malli OpenOffice Draw formur modèle OpenOffice Draw teimpléad OpenOffice Draw modelo de OpenOffice Draw תבנית של OpenOffice Draw Predložak OpenOffice Drawa OpenOffice Draw sablon Patrono OpenOffice Draw Templat OpenOffice Draw Modello OpenOffice Draw OpenOffice Draw テンプレート OpenOffice Draw-ის შაბლონი OpenOffice Draw үлгісі OpenOffice Draw 그림 문서 서식 OpenOffice Draw šablonas OpenOffice Draw veidne OpenOffice Draw-mal OpenOffice.org Draw-sjabloon OpenOffice Draw-mal modèl OpenOffice Draw Szablon rysunku OpenOffice.org Draw modelo OpenOffice Draw Modelo do OpenOffice Draw Șablon OpenOffice Draw шаблон OpenOffice Draw Šablóna OpenOffice Draw Predloga OpenOffice.org Draw Model OpenOffice Draw Шаблон Опен Офис Цртежа OpenOffice Draw-mall OpenOffice Draw şablonu шаблон малюнку OpenOffice Draw Mẫu bản vẽ Draw của OpenOffice.org OpenOffice.org Draw 绘图模板 OpenOffice Draw 範本 OpenOffice Impress presentation عرض تقديمي Impress المكتب المفتوح OpenOffice Impress sənədi Prezentacyja OpenOffice Impress Презентация — OpenOffice Impress presentació d'OpenOffice Impress prezentace OpenOffice Impress Cyflwyniad OpenOffice (Impress) OpenOffice Impress-præsentation OpenOffice-Impress-Vorlage Παρουσίαση OpenOffice Impress OpenOffice Impress presentation presentación de OpenOffice Impress OpenOffice.org Impress aurkezpena OpenOffice Impress -esitys OpenOffice Impress framløga présentation OpenOffice Impress láithreoireacht OpenOffice Impress presentación de de OpenOffice Impress מצגת של OpenOffice Impress OpenOffice Impress prezentacija OpenOffice Impress bemutató Presentation OpenOffice Impress Presentasi OpenOffice Impress Presentazione OpenOffice Impress OpenOffice Impress プレゼンテーション OpenOffice Impress-ის პრეზენტაცია OpenOffice Impress презентациясы OpenOffice Impress 프레젠테이션 OpenOffice Impress pateiktis OpenOffice Impress prezentācija OpenOffice Impress-presentasjon OpenOffice.org Impress-presentatie OpenOffice Impress-presentasjon presentacion OpenOffice Impress Prezentacja OpenOffice.org Impress apresentação OpenOffice Impress Apresentação do OpenOffice Impress Prezentare OpenOffice Impress презентация OpenOffice Impress Prezentácia OpenOffice Impress Predstavitev OpenOffice.org Impress Prezantim OpenOffice Impress Презентација Опен Офис Импреса OpenOffice Impress-presentation OpenOffice Impress sunumu презентація OpenOffice Impress Trình diễn Impress của OpenOffice.org OpenOffice.org Impress 演示文稿 OpenOffice Impress 簡報 OpenOffice Impress template قالب Impress المكتب المفتوح Šablon OpenOffice Impress Шаблон за презентации — OpenOffice Impress plantilla d'OpenOffice Impress šablona OpenOffice Impress OpenOffice Impress-skabelon OpenOffice-Impress-Vorlage Πρότυπο OpenOffice Impress OpenOffice Impress template plantilla de OpenOffice Impress OpenOffice Impress txantiloia OpenOffice Impress -malli OpenOffice Impress formur modèle OpenOffice Impress teimpléad OpenOffice Impress modelo de OpenOffice Impress תבנית של OpenOffice Impress Predložak OpenOffice Impressa OpenOffice Impress sablon Patrono OpenOffice Impress Templat OpenOffice Impress Modello OpenOffice Impress OpenOffice Impress テンプレート OpenOffice Impress-ის შაბლონი OpenOffice Impress үлгісі OpenOffice Impress 프레젠테이션 문서 서식 OpenOffice Impress šablonas OpenOffice Impress veidne OpenOffice Impress-mal OpenOffice.org Impress-sjabloon OpenOffice Impress-mal modèl OpenOffice Impress Szablon prezentacji OpenOffice.org Impress modelo OpenOffice Impress Modelo do OpenOffice Impress Șablon OpenOffice Impress шаблон OpenOffice Impress Šablóna OpenOffice Impress Predloga OpenOffice.org Impress Model OpenOffice Impress Шаблон Опен Офис Импреса OpenOffice Impress-mall OpenOffice Impress şablonu шаблон презентації OpenOffice Impress Mẫu trình diễn Impress của OpenOffice.org OpenOffice.org Impress 演示文稿模板 OpenOffice Impress 範本 OpenOffice Math formula صيغة Math المكتب المفتوح Formuła OpenOffice Math Формула — OpenOffice Math fórmula d'OpenOffice Math vzorec OpenOffice Math OpenOffice Math-formel OpenOffice-Math-Formel Μαθηματικός τύπος OpenOffice Math OpenOffice Math formula fórmula de OpenOffice Math OpenOffice.org Math formula OpenOffice Math -kaava OpenOffice Math frymil formule OpenOffice Math foirmle OpenOffice Math fórmula de OpenOffice Math נוסחה של OpenOffice Math OpenOffice Math formula OpenOffice Math képlet Formula OpenOffice Math Formula OpenOffice Math Formula OpenOffice Math OpenOffice Math 計算式 OpenOffice Math-ის ფორმულა OpenOffice Math формуласы OpenOffice Math 수식 OpenOffice Math formulė OpenOffice Math formula OpenOffice Math-formel OpenOffice.org Math-formule OpenOffice Math-formel formula OpenOffice Math Formuła OpenOffice.org Math fórmula OpenOffice Math Fórmula do OpenOffice Math Formulă OpenOffice Math формула OpenOffice Math Vzorec OpenOffice Math Dokument formule OpenOffice.org Math Formulë OpenOffice Math Формула Опен Офис Математике OpenOffice Math-formel OpenOffice Math formülü формула OpenOffice Math Công thức Math của OpenOffice.org OpenOffice.org Math 公式 OpenOffice Math 公式 OpenOffice Writer document مستند Writer المكتب المفتوح OpenOffice Writer sənədi Dakument OpenOffice Writer Документ — OpenOffice Writer document d'OpenOffice Writer dokument OpenOffice Writer Dogfen OpenOffice (Writer) OpenOffice Writer-dokument OpenOffice-Writer-Dokument Έγγραφο OpenOffice Writer OpenOffice Writer document documento de OpenOffice Writer OpenOffice.org Writer dokumentua OpenOffice Writer -asiakirja OpenOffice Writer skjal document OpenOffice Writer cáipéis OpenOffice Writer documento de OpenOffice Writer מסמך של OpenOffice Writer OpenOffice Writer dokument OpenOffice Writer dokumentum Documento OpenOffice Writer Dokumen OpenOffice Writer Documento OpenOffice Writer OpenOffice Writer ドキュメント OpenOffice Writer-ის დოკუმენტი OpenOffice Writer құжаты OpenOffice Writer 문서 OpenOffice Writer dokumentas OpenOffice Writer dokuments OpenOffice Writer-dokument OpenOffice.org Writer-document OpenOffice Writer-dokument document OpenOffice Writer Dokument OpenOffice.org Writer documento OpenOffice Writer Documento do OpenOffice Writer Document OpenOffice Writer документ OpenOffice Writer Dokument OpenOffice Writer Dokument OpenOffice.org Writer Dokument OpenOffice Writer Документ Опен Офис Писца OpenOffice Writer-dokument OpenOffice Writer belgesi документ OpenOffice Writer Tài liệu Writer của OpenOffice.org OpenOffice.org Writer 文档 OpenOffice Writer 文件 OpenOffice Writer global document مستند المكتب المفتوح Writer العالمي OpenOffice Writer qlobal sənədi Hlabalny dakument OpenOffice Writer Документ - глобален — OpenOffice Writer document global d'OpenOffice Writer globální dokument OpenOffice Writer Dogfen eang OpenOffice (Writer) OpenOffice Writer-globalt dokument OpenOffice-Writer-Globaldokument Καθολικό έγγραφο OpenOffice Writer OpenOffice Writer global document documento global de OpenOffice Writer OpenOffice.org Writer dokumentu globala OpenOffice Writer - yleinen asiakirja OpenOffice Writer heiltøkt skjal document global OpenOffice Writer cáipéis chomhchoiteann OpenOffice Writer documento global de OpenOffice Writer מסמך גלובלי של OpenOffice Writer OpenOffice Writer globalni dokument OpenOffice Writer globális dokumentum Documento global OpenOffice Writer Dokumen global OpenOffice Writer Documento globale OpenOffice Writer OpenOffice Writer グローバルドキュメント OpenOffice Writer-ის გლობალური დოკუმენტი OpenOffice Writer негізгі құжаты OpenOffice Writer 글로벌 문서 OpenOffice Writer bendrinis dokumentas OpenOffice Writer globālais dokuments Global OpenOffice Writer globalt dokument OpenOffice.org Writer-globaal-document OpenOffice Writer globalt dokument document global OpenOffice Writer Globalny dokument OpenOffice.org Writer documento global OpenOffice Writer Documento global do OpenOffice Writer Document global OpenOffice Writer основной документ OpenOffice Writer Globálny dokument OpenOffice Writer Splošni dokument OpenOffice.org Writer Dokument i përgjithshëm OpenOffice Writer Општи документ Опен Офис Писца OpenOffice Writer-globaldokument OpenOffice Writer global belgesi загальний документ OpenOffice Writer Tài liệu toàn cục Writer của OpenOffice.org OpenOffice.org Writer 全局文档 OpenOffice Writer 主控文件 OpenOffice Writer template قالب Writer المكتب المفتوح OpenOffice Writer şablonu Šablon OpenOffice Writer Шаблон за документи — OpenOffice Writer plantilla d'OpenOffice Writer šablona OpenOffice Writer Templed OpenOffice (Writer) OpenOffice Writer-skabelon OpenOffice-Writer-Vorlage Πρότυπο OpenOffice Writer OpenOffice Writer template plantilla de OpenOffice Writer OpenOffice Writer txantiloia OpenOffice Writer -malli OpenOffice Writer formur modèle OpenOffice Writer teimpléad OpenOffice Writer modelo de OpenOffice Writer תסנית של OpenOffice Writer OpenOffice Writer predložak OpenOffice Writer sablon Patrono OpenOffice Writer Templat OpenOffice Writer Modello OpenOffice Writer OpenOffice Writer ドキュメントテンプレート OpenOffice Writer-ის შაბლონი OpenOffice Writer үлгісі OpenOffice Writer 문서 서식 OpenOffice Writer šablonas OpenOffice Writer veidne Templat OpenOffice Writer OpenOffice Writer-mal OpenOffice.org Writer-sjabloon OpenOffice Writer-mal modèl OpenOffice Writer Szablon dokumentu OpenOffice.org Writer modelo OpenOffice Writer Modelo do OpenOffice Writer Șablon OpenOffice Writer шаблон OpenOffice Writer Šablóna OpenOffice Writer Predloga OpenOffice.org Writer Model OpenOffice Writer Шаблон Опен Офис Писца OpenOffice Writer-mall OpenOffice Writer şablonu шаблон документа OpenOffice Writer Mẫu tài liệu Writer của OpenOffice.org OpenOffice.org Writer 文档模板 OpenOffice Writer 範本 ODT document مستند ODT Dakument ODT Документ — ODT document ODT dokument ODT ODT-dokument ODT-Dokument Έγγραφο ODT ODT document ODT-dokumento documento ODT ODT dokumentua ODT-asiakirja ODT skjal document ODT cáipéis ODT documento ODT מסמך ODT ODT dokument ODT-dokumentum Documento ODT Dokumen ODT Documento ODT ODT ドキュメント ODT დოკუმენტი ODT құжаты ODT 문서 ODT dokumentas ODT dokuments ODT-dokument ODT-document ODT-dokument document ODT Dokument ODT documento ODT Documento ODT Document ODT документ ODT Dokument ODT Dokument ODT Dokument ODT ОДТ документ ODT-dokument ODT belgesi документ ODT Tài liệu ODT ODT 文档 ODT 文件 ODT OpenDocument Text ODT document (Flat XML) مستند ODT (Flat XML) Документ — ODT (само XML) document ODT (XML pla) dokument ODT (Flat XML) ODT-dokument (flad XML) ODT-Dokument (Unkomprimiertes XML) Έγγραφο ODT (Flat XML) ODT document (Flat XML) documento ODT (XML plano) ODT dokumentua (XML soila) ODT-asiakirja (Flat XML) ODT skjal (Flat XML) document ODT (XML plat) cáipéis ODT (XML cothrom) documento ODT (XML plano) מסמך ODT‏ (Flat XML) ODT dokument (Flat XML) ODT-dokumentum (egyszerű XML) Documento ODT (XML platte) Dokumen ODT (Flat XML) Documento ODT (XML semplice) ODT ドキュメント (Flat XML) ODT დოკუმენტი (Flat XML) ODT құжаты (Тек XML) ODT 문서(단일 XML) ODT dokumentas (Flat XML) ODT dokuments (plakans XML) ODT document (Flat XML) document ODT (XML plat) Dokument ODT (prosty XML) documento ODT (XML plano) Documento ODT (Flat XML) Document ODT (XML simplu) документ ODT (простой XML) Dokument ODT (čisté XML) Datoteka dokumenta ODT (nepovezan XML) ОДТ документ (Обични ИксМЛ) ODT-dokument (platt XML) ODT belgesi (Düz XML) документ ODT (Flat XML) ODT 文档(Flat XML) ODT 文件 (Flat XML) FODT OpenDocument Text (Flat XML) ODT template قالب ODT Šablon ODT Шаблон за документи — ODT plantilla ODT šablona ODT ODT-skabelon ODT-Vorlage Πρότυπο ODT ODT template ODT-ŝablono plantilla ODT ODT txantiloia ODT-malli ODT formur modèle ODT teimpléad ODT modelo ODT תבנית ODT ODT predložak ODT-sablon Patrono ODT Templat ODT Modello ODT ODT テンプレート ODT დოკუმენტი ODT үлгісі ODT 문서 서식 ODT šablonas ODT veidne ODT-mal ODT-sjabloon ODT-mal modèl ODT Szablon ODT modelo ODT Modelo ODT Șablon ODT шаблон ODT Šablóna ODT Predloga dokumenta ODT Model ODT ОДТ шаблон ODT-mall ODT şablonu шаблон ODT Mẫu ODT ODT 模板 ODT 範本 ODT OpenDocument Text OTH template قالب OTH Šablon OTH Шаблон за страници — OTH plantilla OTH šablona OTH OTH-skabelon OTH-Vorlage Πρότυπο OTH OTH template OTH-ŝablono plantilla OTH OTH txantiloia OTH-malli OTH formur modèle OTH teimpléad OTH modelo OTH תבנית OTH OTH predložak OTH-sablon Patrono OTH Templat OTH Modello OTH OTH テンプレート OTH შაბლონი OTH үлгісі OTH 문서 서식 OTH šablonas OTH veidne OTH-mal OTH-sjabloon OTH-mal modèl OTH Szablon OTH modelo OTH Modelo OTH Șablon OTH шаблон OTH Šablóna OTH Predloga OTH Model OTH ОТХ шаблон OTH-mall OTH şablonu шаблон OTH Mẫu ODH OTH 模板 OTH 範本 OTH OpenDocument HTML ODM document مستند ODM Dakument ODM Документ — ODM document ODM dokument ODM ODM-dokument ODM-Dokument Έγγραφο ODM ODM document ODM-dokumento documento ODM ODM dokumentua ODM-asiakirja ODM skjal document ODM cáipéis ODM documento ODM מסמך ODM ODM dokument ODM-dokumentum Documento ODM Dokumen ODM Documento ODM ODM ドキュメント ODM დოკუმენტი ODM құжаты ODM 문서 ODM dokumentas ODM dokuments ODM-dokument ODM-document ODM-dokument document ODM Dokument ODM documento ODM Documento ODM Document ODM документ ODM Dokument ODM Dokument ODM Dokument ODM ОДМ документ ODM-dokument ODM belgesi документ ODM Tài liệu ODM ODM 文档 ODM 文件 ODM OpenDocument Master ODG drawing تصميم ODG Rysunak ODG Чертеж — ODG dibuix ODG kresba ODG ODG-tegning ODG-Zeichnung Σχέδιο ODG ODG drawing ODG-desegnaĵo dibujo ODG ODG marrazkia ODG-piirros ODG tekning dessin ODG líníocht ODG debuxo ODG ציור ODG ODG crtež ODG-rajz Designo ODG Gambar ODG Disegno ODG ODG ドロー ODG-ის ნახაზი ODG суреті ODG 드로잉 ODG piešinys ODG zīmējums ODG-tegning ODG-tekening ODG-teikning dessenh ODG Rysunek ODG desenho ODG Desenho ODG Desen ODG изображение ODG Kresba ODG Datoteka risbe ODG Vizatim ODG ОДГ цртеж ODG-teckning ODG çizimi малюнок ODG Bản vẽ ODG ODG 绘图 ODG 繪圖 ODG OpenDocument Drawing ODG drawing (Flat XML) رسمة ODG (Flat XML) Чертеж — ODG (само XML) dibuix ODG (XML pla) kresba ODG (Flat XML) ODG-tegning (flad XML) ODG-Zeichnung (Unkomprimiertes XML) Σχέδιο ODG (Flat XML) ODG drawing (Flat XML) dibujo ODG (XML plano) ODG marrazkia (XML soila) ODG-piirros (Flat XML) ODG tekning (Flat XML) dessin ODG (XML plat) líníocht ODG (XML cothrom) debuxo ODB (XML plano) ציור ODG (Flat XML( ODG crtež (Flat XML) ODG-rajz (egyszerű XML) Designo ODG (XML platte) Gambar ODG (FLAT XML) Disegno ODG (XML semplice) ODG ドロー (Flat XML) ODG-ის ნახაზი (Flat XML) ODG сызбасы (Тек XML) ODG 드로잉(단일 XML) ODG piešinys (Flat XML) ODG zīmējums (plakans XML) ODG-tekening (Flat XML) dessenh ODG (XML plat) Rysunek ODG (prosty XML) desenho ODG (XML plano) Desenho ODG (Flat XML) Desen ODG (XML simplu) изображение ODG (простой XML) Kresba ODG (čisté XML) Datoteka risbe ODG (nepovezan XML) ОДГ цртеж (Обичан ИксМЛ) ODG-teckning (platt XML) ODG çizimi (Düz XML) малюнок ODG (Flat XML) ODG 绘图(Flat XML) ODG 繪圖 (Flat XML) FODG OpenDocument Drawing (Flat XML) ODG template قالب ODG Šablon ODG Шаблон за чертежи — ODG plantilla ODG šablona ODG ODG-skabelon ODG-Vorlage Πρότυπο ODG ODG template ODG-ŝablono plantilla ODG ODG txantiloia ODG-malli ODG formur modèle ODG teimpléad ODG modelo ODG תבנית ODG ODG predložak ODG-sablon Patrono ODG Templat ODG Modello ODG ODG テンプレート ODG-ის შაბლონი ODG үлгісі ODG 문서 서식 ODG šablonas ODG veidne ODG-mal ODG-sjabloon ODG-mal modèl ODG Szablon ODG modelo ODG Modelo ODG Șablon ODG шаблон ODG Šablóna ODG Predloga dokumenta ODG Model ODG ОДГ шаблон ODG-mall ODG şablonu шаблон ODG Mẫu ODG ODG 模板 ODG 範本 ODG OpenDocument Drawing ODP presentation عرض تقديمي ODP Prezentacyja ODP Презентация — ODP presentació ODP prezentace ODP ODP-præsentation ODP-Präsentation Παρουσίαση ODP ODP presentation ODP-prezentaĵo presentación ODP ODP aurkezpena ODP-esitys ODP framløga présentation ODP láithreoireacht ODP presentación ODP מצגת ODP ODP prezentacija ODP-prezentáció Presentation ODP Presentasi ODP Presentazione ODP ODP プレゼンテーション ODP პრეზენტაცია ODP презентациясы ODP 프레젠테이션 ODP pateiktis ODP prezentācija ODP-presentasjon ODP-presentatie ODP-presentasjon presentacion ODP Prezentacja ODP apresentação ODP Apresentação ODP Prezentare ODP презентация ODP Prezentácia ODP Predstavitev ODP Prezantim ODP ОДП презентација ODP-presentation ODP sunumu презентація ODP Trình diễn ODM ODP 演示文稿 ODP 簡報 ODP OpenDocument Presentation ODP presentation (Flat XML) عرض ODP (Flat XML) Презентация — ODP (само XML) presentació ODP (XML pla) prezentace ODP (Flat XML) ODP-præsentation (flad XML) ODP-Präsentation (Unkomprimiertes XML) Παρουσίαση ODP (Flat XML) ODP presentation (Flat XML) presentación ODP (XML plano) ODP aurkezpena (XML soila) ODP-esitys (Flat XML) ODP framløga (Flat XML) présentation ODP (XML plat) láithreoireacht ODP (XML cothrom) presentación ODP (XML plano) מצגת ODP‏ (Flat XML) ODP prezentacija (Flat XML) ODP-prezentáció (egyszerű XML) Presentation ODP (XML platte) Presentasi ODP (Flat XML) Presentazione ODP (XML semplice) ODP プレゼンテーション (Flat XML) ODP პრეზენტაცია (Flat XML) ODP презентациясы (Тек XML) ODP 프레젠테이션(단일 XML) ODP pateiktis (Flat XML) ODP prezentācija (plakans XML) ODP presentatie (Flat XML) presentacion ODP (XML plat) Prezentacja ODP (prosty XML) apresentação ODP (XML plano) Apresentação ODP (Flat XML) Prezentare ODP (XML simplu) презентация ODP (простой XML) Prezentácia ODP (čisté XML) Predstavitev ODP (nepovezan XML) ОДП презентација (Обични ИксМЛ) ODP-presentation (platt XML) ODP sunumu (Düz XML) презентація ODP (Flat XML) ODP 演示文稿(Flat XML) ODP 範本 (Flat XML) FODP OpenDocument Presentation (Flat XML) ODP template قالب ODP Šablon ODP Шаблон за презентации — ODP plantilla ODP šablona ODP ODP-skabelon ODP-Vorlage Πρότυπο ODP ODP template ODP-ŝablono plantilla ODP ODP txantiloia ODP-malli ODP formur modèle ODP teimpléad ODP modelo ODP תבנית ODP ODP predložak ODP-sablon Patrono ODP Templat ODP Modello ODP ODP テンプレート ODP შაბლონი ODP үлгісі ODP 문서 서식 ODP šablonas ODP veidne ODP-mal ODP-sjabloon ODP-mal modèl ODP Szablon ODP modelo ODP Modelo ODP Șablon ODP шаблон ODP Šablóna ODP Predloga dokumenta ODP Model ODP ОДП шаблон ODP-mall ODP şablonu шаблон ODP Mẫu ODP ODP 模板 ODP 範本 ODP OpenDocument Presentation ODS spreadsheet جدول ODS Raźlikovy arkuš ODS Таблица — ODS full de càlcul ODS sešit ODS ODS-regneark ODS-Tabelle Λογιστικό φύλλο ODS ODS spreadsheet ODS-kalkultabelo hoja de cálculo ODS ODS kalkulu-orria ODS-taulukko ODS rokniark feuille de calcul ODS scarbhileog ODS folla de cálculo ODS גליון נתונים ODS ODS proračunska tablica ODS-táblázat Folio de calculo ODS Lembar sebar ODS Foglio di calcolo ODS ODS スプレッドシート ODS ცხრილი ODS электрондық кестесі ODS 스프레드시트 ODS skaičialentė ODS izklājlapa ODS-regneark ODS-rekenblad ODS-rekneark fuèlh de calcul ODS Arkusz ODS folha de cálculo ODS Planilha ODS Foaie de calcul ODS электронная таблица ODS Zošit ODS Preglednica ODS Fletë llogaritjesh ODS ОДС spreadsheet ODS-kalkylblad ODS çalışma sayfası ел. таблиця ODS Bảng tính ODS ODS 工作簿 ODS 試算表 ODS OpenDocument Spreadsheet ODS spreadsheet (Flat XML) جدول ODS (Flat XML) Таблица — ODS (само XML) full de càlcul ODS (XML pla) sešit ODS (Flat XML) ODS-regneark (flad XML) ODS-Tabelle (Unkomprimiertes XML) Λογιστικό φύλλο ODS (Flat XML) ODS spreadsheet (Flat XML) hoja de cálculo ODS (XML plano) ODS kalkulu-orria (XML soila) ODS-laskentataulukko (Flat XML) ODS rokniark (Flat XML) feuille de calcul ODS (XML plat) scarbhileog ODS (XML cothrom) folla de cálculo ODS (XML plano) גליון נתונים ODS‏ (XML פשוט) ODS proračunska tablica (Flat XML) ODS-táblázat (egyszerű XML) Folio de calculo ODS (XML platte) Lembar sebar ODS (Flat XML) Foglio di calcolo ODS (XML semplice) ODS スプレッドシート (Flat XML) ODS ცხრილი (Flat XML) ODS электрондық кестесі (Тек XML) ODS 스프레드시트(단일 XML) ODS skaičialentė (Flat XML) ODS izklājlapa (plakans XML) ODS spreadsheet (Flat XML) fuèlh de calcul ODS (XML plat) Arkusz ODS (prosty XML) folha de cálculo ODS (XML plano) Planilha ODS (Flat XML) Foaie de calcul ODS (XML simplu) электронная таблица ODS (простой XML) Zošit ODS (čisté XML) Preglednica ODS (nepovezan XML) ОДС spreadsheet (Обични ИксМЛ) ODS-kalkylblad (platt XML) ODS sunumu (Düz XML) ел. таблиця ODS (Flat XML) ODS 工作簿(Flat XML) ODS 試算表 (Flat XML) FODS OpenDocument Spreadsheet (Flat XML) ODS template قالب ODS Šablon ODS Шаблон за таблици — ODS plantilla ODS šablona ODS ODS-skabelon ODS-Vorlage Πρότυπο ODS ODS template ODS-ŝablono plantilla ODS ODS txantiloia ODS-malli ODS formur modèle ODS teimpléad ODS modelo ODS תבנית ODS ODS predložak ODS-sablon Patrono ODS Templat ODS Modello ODS ODS テンプレート ODS-ის შაბლონი ODS үлгісі ODS 문서 서식 ODS šablonas ODS veidne ODS-mal ODS-sjabloon ODS-mal modèl ODS Szablon ODS modelo ODS Modelo ODS Șablon ODS шаблон ODS Šablóna ODS Predloga dokumenta ODS Model ODS ОДС шаблон ODS-mall ODS şablonu шаблон ODS Mẫu ODS ODS 模板 ODS 範本 ODS OpenDocument Spreadsheet ODC chart مخطط ODC Dyjahrama ODC Диаграма — ODC diagrama ODC graf ODC ODC-diagram ODC-Diagramm Διάγραμμα ODC ODC chart ODC-diagramo gráfico ODC ODC diagrama ODC-kaavio ODC strikumynd graphique ODC cairt ODC gráfica ODC תו ODC ODC grafikon ODC-táblázat Graphico ODC Bagan ODC Grafico ODC ODC チャート ODC диаграммасы ODC 차트 ODC diagrama ODC diagramma ODC-graf ODC-grafiek ODC-diagram grafic ODC Wykres ODC gráfico ODC Gráfico ODC Diagramă ODC диаграмма ODC Graf ODC Datoteka grafikona ODC Grafik ODC ОДЦ chart ODC-diagram ODC çizelgesi діаграма ODC Sơ đồ ODC ODC 图表 ODC 圖表 ODC OpenDocument Chart ODC template قالب ODC Шаблон за диаграми — ODC plantilla ODC šablona ODC ODC-skabelon ODC-Vorlage Πρότυπο ODC ODC template ODC-ŝablono plantilla ODC ODC txantiloia ODC-malli ODC formur modèle ODC teimpléad ODC modelo ODC תבנית ODC ODC predložak ODC-sablon Patrono ODC Templat ODC Modello ODC ODC テンプレート ODC შაბლონი ODC үлгісі ODC 문서 서식 ODC šablonas ODC veidne ODC-sjabloon modèl ODC Szablon ODC modelo ODC Modelo ODC Șablon ODC шаблон ODC Šablóna ODC Predloga ODC ОДЦ шаблон ODC-mall ODC şablonu шаблон ODC Mẫu ODC ODC 模板 ODC 範本 ODC OpenDocument Chart ODF formula صيغة ODF Formuła ODF Формула — ODF fórmula ODF vzorec ODF ODF-formel ODF-Formel Μαθηματικός τύπος ODF ODF formula ODF-formulo fórmula ODF ODF formula ODF-kaava ODF frymil formule ODF foirmle ODF Fórula ODF נוסחת ODF ODF formula ODF-képlet Formula ODF Formula ODF Formula ODF ODF 計算式 ODF-ის ფორმულა ODF формуласы ODF 수식 ODF formulė ODF formula ODF-formel ODF-formule ODF-formel formula ODF Formuła ODF fórmula ODF Fórmula ODF Formulă ODF формула ODF Vzorec ODF Dokument formule ODF Formulë ODF ОДФ formula ODF-formel ODF formülü формула ODF Công thức ODF ODF 公式 ODF 公式 ODF OpenDocument Formula ODF template قالب ODF Шаблон за формули — ODF plantilla ODF šablona ODF ODF-skabelon ODF-Vorlage Πρότυπο ODF ODF template ODF-ŝablono plantilla ODF ODF txantiloia ODF-malli ODF formur modèle ODF teimpléad ODF modelo ODF תבנית ODF ODF predložak ODG-sablon Patrono ODF Templat ODF Modello ODF ODF テンプレート ODF-ის შაბლონი ODF үлгісі ODF 문서 서식 ODF šablonas ODF veidne ODF-sjabloon modèl ODF Szablon ODF modelo ODF Modelo ODF Șablon ODF шаблон ODF Šablóna ODF Predloga dokumenta ODF ОДФ шаблон ODF-mall ODF şablonu шаблон ODF Mẫu ODF ODF 模板 ODF 範本 ODF OpenDocument Formula ODB database قاعدة بيانات ODB Baza źviestak ODB База от данни — ODB base de dades ODB databáze ODB ODB-database ODB-Datenbank Βάση δεδομένων ODB ODB database ODB-datumbazo base de datos ODB ODB datu-basea ODB-tietokanta ODB dátustovnur base de données ODB bunachar sonraí ODB base de datos ODB מסד נתונים ODB ODB baza podataka ODB-adatbázis Base de datos ODB Basis data ODB Database ODB ODB データベース ODB-ის მონაცემთა ბაზა ODB дерекқоры ODB 데이터베이스 ODB duomenų bazė ODB datubāze ODB-database ODB-gegevensbank ODB-database banca de donadas ODB Baza danych ODB base de dados ODB Banco de dados ODB Bază de date ODB база данных ODB Databáza ODB Podatkovna zbirka ODB Bazë me të dhëna ODB ОДБ база података ODB-databas ODB veritabanı база даних ODB Cơ sở dữ liệu ODB ODB 数据库 ODB 資料庫 ODB OpenDocument Database ODI image صورة ODI Vyjava ODI Изображение — ODI imatge ODI obrázek ODI ODI-billede ODI-Bild Εικόνα ODI ODI image ODI-bildo imagen ODI ODI irudia ODI-kuva ODI mynd image ODI íomhá ODI imaxe ODI תמונת ODI ODI slika ODI-kép Imagine ODI Citra ODI Immagine ODI ODI 画像 ODI გამოსახულება ODI суреті ODI 그림 ODI paveikslėlis ODI attēls ODI-bilde ODI-afbeelding ODI-bilete imatge ODI Obraz ODI imagem ODI Imagem ODI Imagine ODI изображение ODI Obrázok ODI Slikovna datoteka ODI Figurë ODI ОДИ слика ODI-bild ODI görüntüsü зображення ODI Ảnh ODI ODI 图像 ODI 影像 ODI OpenDocument Image OpenOffice.org extension امتداد OpenOffice.org Pašyreńnie OpenOffice.org Разширение — OpenOffice extensió d'OpenOffice.org rozšíření OpenOffice.org OpenOffice.org-udvidelse OpenOffice.org-Erweiterung Επέκταση OpenOffice.org OpenOffice.org extension extensión de LibreOffice OpenOffice.org luzapena OpenOffice.org-laajennus OpenOffice.org víðkan extension OpenOffice.org eisínteacht OpenOffice.org Extensión de OpenOffice.org הרחבה של OpenOffice.org OpenOffice.org proširenje OpenOffice.org kiterjesztés Extension OpenOffice.org Ekstensi OpenOffice.org Estensione OpenOffice.org OpenOffice.org 拡張機能 OpenOffice.org-ის გაფართოება OpenOffice.org кеңейтуі OpenOffice.org 확장 OpenOffice.org plėtinys OpenOffice.org paplašinājums OpenOffice.org-uitbreiding OpenOffice Writer-utviding extension OpenOffice.org Rozszerzenie OpenOffice.org extensão OpenOffice.org Extensão do OpenOffice Extensie OpenOffice.org расширение OpenOffice.org Rozšírenie OpenOffice.org Razširitev OpenOffice.org Shtojcë për OpenOffice.org проширење ОпенОфис.орг-а OpenOffice.org-tillägg OpenOffice.org eklentisi розширення OpenOffice.org Phần mở rộng của OpenOffice.org OpenOffice.org 扩展 OpenOffice.org 擴充套件 Android package Пакет — Android paquet d'Android balíčky systému Android Android-pakke Android-Paket Πακέτο Android Android package Android-pakaĵo paquete de Android Android paketea Android-paketti paquet Android paquete de Android חבילת אנדרויד Android paket Android csomag Pacchetto Android Paket Android Pacchetto Android Android パッケージ Android-ის პაკეტი Android дестесі Android 패키지 Android pakotne Android pakket paquet Android Pakiet Androida pacote Android Pacote do Android пакет Android Balík Android Paket Android Андроидов пакет Android-paket Android paketi пакунок Android Android Android 軟體包 SIS package حزمة SIS Pakunak SIS Пакет — SIS paquet SIS balíček SIS SIS-pakke SIS-Paket Πακέτο SIS SIS package SIS-pakaĵo paquete SIS SIS paketea SIS-paketti SIS pakki paquet SIS pacáiste SIS paquete SIS חבילת SIS SIS paket SIS csomag Pacchetto SIS Paket SIS Pacchetto SIS SIS パッケージ SIS дестесі SIS 패키지 SIS paketas SIS pakotne SIS-pakke SIS-pakket SIS-pakke paquet SIS Pakiet SIS pacote SIS Pacote SIS Pachet SIS пакет SIS Balíček SIS Datoteka paketa SIS Paketë SIS СИС пакет SIS-paket SIS paketi пакунок SIS Gói SIS SIS 软件包 SIS 軟體包 SIS Symbian Installation File SISX package حزمة SISX Pakunak SISX Пакет — SISX paquet SISX balíček SISX SISX-pakke SISX-Paket Πακέτο SISX SISX package SISX-pakaĵo paquete SISX SISX paketea SISX-paketti SISX pakki paquet SISX pacáiste SISX paquete SISX חבילת SISX SISX paket SISX csomag Pacchetto SISX Paket SISX Pacchetto SISX SISX パッケージ SISX дестесі SISX 패키지 SISX paketas SISX pakotne SISX-pakke SISX-pakket SISX-pakke paquet SISX Pakiet SISX pacote SISX Pacote SISX Pachet SISX пакет SISX Balíček SISX Datoteka paketa SISX Paketë SISX СИСИкс пакет SISX-paket SISX paketi пакунок SISX Gói SISX SISX 软件包 SISX 軟體包 SIS Symbian Installation File Network Packet Capture Прихванати пакети по мрежата captura de paquets de xarxa Network Packet Capture Netværkspakkeoptegnelse Netzwerk-Paketmitschnitt Σύλληψη πακέτων δικτύου Network Packet Capture captura de paquete de red Sareko pakete kaptura Verkkopakettien kaappaus capture de paquet réseau Captura de Network Packet לכידה של מנות נתונים ברשת Mrežno hvatanje paketa Hálózati csomagelfogás Captura de pacchettos de rete Tangkapan Paket Jaringan Cattura pacchetti rete ネットワークパケットキャプチャー ქსელური პაკეტის ანაბეჭდი ұсталған желілік пакеттер 네트워크 패킷 캡처 Network Packet Capture Network Packet Capture captura de paquet ret Przechwycenie pakietu sieciowego captura Network Packet Pacote de captura de rede захваченные сетевые пакеты Zachytené sieťové pakety Zajem omrežnih paketov Снимање мрежног пакета Fångst av nätverkspaket Ağ Paket Yakalaması перехоплені дані мережевих пакетів 网络包抓取 網路封包捕捉 WordPerfect document مستند WordPerfect WordPerfect sənədi Dakument WordPerfect Документ — WordPerfect document WordPerfect dokument WordPerfect Dogfen WordPerfect WordPerfect-dokument WordPerfect-Dokument Έγγραφο WordPerfect WordPerfect document WordPerfect-dokumento documento de WordPerfect WordPerfect dokumentua WordPerfect-asiakirja WordPerfect skjal document WordPerfect cáipéis WordPerfect documento de WordPerfect מסמך WordPerfect WordPerfect dokument WordPerfect-dokumentum Documento WordPerfect Dokumen WordPerfect Documento WordPerfect WordPerfect ドキュメント WordPerfect құжаты WordPerfect 문서 WordPerfect dokumentas WordPerfect dokuments Dokumen WordPerfect WordPerfect-dokument WordPerfect-document WordPerfect-dokument document WordPerfect Dokument WordPerfect documento WordPerfect Documento do WordPerfect Document WordPerfect документ WordPerfect Dokument WordPerfect Dokument WordPerfect Dokument WordPerfect документ Ворд Перфекта WordPerfect-dokument WordPerfect belgesi документ WordPerfect Tài liệu WordPerfect WordPerfect 文档 WordPerfect 文件 SPSS Portable Data File ملف بيانات SPSS متنقلة Данни — SPSS, преносими fitxer de dades portables SPSS soubor přenositelných dat SPSS Portabel SPSS-datafil SPSS portable Datendatei Φορητό αρχείο δεδομένων SPSS SPSS Portable Data File archivo de datos portátil de SPSS SPSS datuen fitxategi eramangarria SPSS flytifør dátufíla fichier portable de données SPSS comhad iniompartha sonraí SPSS ficheiro de datos portábel SPSS קובץ מידע נייד SPSS SPSS prenosiva podatkovna datoteka SPSS hordozható adatfájl File portabile de datos SPSS Berkas Data Portabel SPSS File dati SPSS Portable SPSS ポータブルデータファイル SPSS тасымалы ақпарат файлы SPSS 이동식 데이터 파일 SPSS perkeliamų duomenų failas SPSS pārvietojamu datu datne SPSS Portable Databestand fichièr portable de donadas SPSS Plik przenośnych danych SPSS ficheiro de dados portátil SPSS Arquivo de Dados Portáteis SPSS Fișier portabil de date SPSS файл переносимых данных SPSS Súbor prenosných dát SPSS Prenosna podatkovna datoteka SPSS СПСС датотека преносних података Portabel SPSS-datafil SPSS Taşınabilir Veri Dosyası файл даних SPSS Portable SPSS 便携式数据文件 SPSS 可攜式資料檔 SPSS Data File ملف بيانات SPSS Данни — SPSS fitxer de dades SPSS datový soubor SPSS SPSS-datafil SPSS-Datendatei Αρχείο δεδομένων SPSS SPSS Data File archivo de datos SPSS SPSS datuen fitxategia SPSS dátufíla fichier de données SPSS comhad sonraí SPSS ficheiro de datos SPSS קובץ מידע SPSS SPSS podatkovna datoteka SPSS adatfájl File de datos SPSS Berkas Data SPSS File dati SPSS SPSS データファイル SPSS ақпарат файлы SPSS 데이터 파일 SPSS duomenų failas SPSS datu datne SPSS Databstand fichièr de donadas SPSS Plik danych SPSS ficheiro de dados SPSS Arquivo de dados SPSS Fișier date SPSS файл данных SPSS Dátový súbor SPSS Podatkovna datoteka SPSS СПСС датотека података SPSS-datafil SPSS Veri Dosyası файл даних SPSS SPSS 数据文件 SPSS 資料檔 XBEL bookmarks علامات XBEL Zakładki XBEL Отметки — XBEL llista d'adreces d'interès XBEL záložky XBEL XBEL-bogmærker XBEL-Lesezeichen Σελιδοδείκτες XBEL XBEL bookmarks XBEL-legosignoj marcadores XBEL XBEL laster-markak XBEL-kirjanmerkit XBEL bókamerki marque-pages XBEL leabharmharcanna XBEL Marcadores XBEL סימניית XBEL XBEL knjižne oznake XBEL-könyvjelzők Marcapaginas XBEL Bookmark XBEL Segnalibri XBEL XBEL ブックマーク XBEL бетбелгілері XBEL 책갈피 XBEL žymelės XBEL grāmatzīmes Tandabuku XBEL XBEL-bokmerker XBEL-bladwijzers XBEL-bokmerker marcapaginas XBEL Zakładki XBEL marcadores XBEL Marcadores do XBEL Semne de carte XBEL закладки XBEL Záložky XBEL Datoteka zaznamkov XBEL Libërshënues XBEL ИксБЕЛ обележивачи XBEL-bokmärken XBEL yer imleri закладки XBEL Liên kết đã lưu XBEL XBEL 书签 XBEL 格式書籤 XBEL XML Bookmark Exchange Language 7-zip archive أرشيف 7-zip Archiŭ 7-zip Архив — 7-zip arxiu 7-zip archiv 7-zip 7-zip-arkiv 7zip-Archiv Συμπιεσμένο αρχείο 7-zip 7-zip archive 7z-arkivo archivador 7-zip 7-zip artxiboa 7-zip-arkisto 7-zip skjalasavn archive 7-zip cartlann 7-zip arquivo 7-zip ארכיון 7-zip 7-zip arhiva 7-zip archívum Archivo 7-zip Arsip 7-zip Archivio 7-zip 7-zip アーカイブ 7-zip არქივი 7-zip архиві 7-ZIP 압축 파일 7-zip archyvas 7-zip arhīvs 7-zip-arkiv 7-zip-archief 7-zip-arkiv archiu 7-zip Archiwum 7-zip arquivo 7-zip Pacote 7-Zip Arhivă 7-zip архив 7-zip Archív 7-zip Datoteka arhiva 7-zip Arkiv 7-zip 7-зип архива 7-zip-arkiv 7-Zip arşivi архів 7-zip Kho nén 7-zip 7-zip 归档文件 7-zip 封存檔 AbiWord document مستند آبي وورد Dakument AbiWord Документ — AbiWord document AbiWord dokument AbiWord AbiWord-dokument AbiWord-Dokument Έγγραφο AbiWord AbiWord document AbiWord-dokumento documento de Abiword AbiWord dokumentua AbiWord-asiakirja AbiWord skjal document AbiWord cáipéis AbiWord documento de AbiWord מסמך AbiWord AbiWord dokument AbiWord-dokumentum Documento AbiWord Dokumen AbiWord Documento AbiWord AbiWord ドキュメント AbiWord-ის დოკუმენტი AbiWord құжаты AbiWord 문서 AbiWord dokumentas AbiWord dokuments Dokumen AbiWord AbiWord-dokument AbiWord-document AbiWord-dokument document AbiWord Dokument AbiWord documento AbiWord Documento do AbiWord Document AbiWord документ AbiWord Dokument AbiWord Dokument AbiWord Dokument AbiWord Абиворд документ AbiWord-dokument AbiWord belgesi документ AbiWord Tài liệu AbiWord AbiWord 文档 AbiWord 文件 CD image cuesheet صفيحة صورة الـCD جديلة Infarmacyjny arkuš vyjavy CD Описание на изображение на CD «cuesheet» d'imatge de CD rozvržení stop obrazu CD Cd-aftrykscuesheet CD-Abbild-Cuesheet Φύλλο cue εικόνας CD CD image cuesheet hoja CUE de imagen de CD CD irudiaren CUE orria CD-vedos cuesheet index de pistes de CD bileog chiúáil íomhá CD cue sheet dunha imaxe de CD גליון נתונים לתמונת דיסק CD slika s meta podacima CD kép cuesheet Indice de pistas de CD Citra cuesheet CD Cuesheet immagine CD CD イメージキューシート CD бейнесінің құрама кестесі CD 이미지 큐시트 CD atvaizdžio aprašas CD attēla rindulapa Filliste for CD-avtrykk CD-inhoudsopgave CD-bilete-indeksfil indèx de pistas de CD Obraz cuesheet płyty CD índice de CD de imagem Índice de Imagem de CD Imagine CD cuesheet таблица содержания образа CD Rozvrhnutie stôp obrazu CD Datoteka razpredelnice odtisa CD cue Cuesheet imazhi CD редослед слика ЦД-а Indexblad för cd-avbild CD görüntüsü belgesi таблиця CUE образу CD Tờ tín hiệu báo ảnh CD CD 映像标记文件 CD 映像指示表 Lotus AmiPro document مستند Lotus AmiPro Lotus AmiPro sənədi Dakument Lotus AmiPro Документ — Lotus AmiPro document de Lotus AmiPro dokument Lotus AmiPro Dogfen Lotus AmiPro Lotus AmiPro-dokument Lotus-AmiPro-Dokument Έγγραφο Lotus AmiPro Lotus AmiPro document dokumento de Lotus AmiPro documento de Lotus AmiPro Lotus AmiPro dokumentua Lotus AmiPro -asiakirja Lotus AmiPro skjal document Lotus AmiPro cáipéis Lotus AmiPro documento de Lotus AmiPro מסמך של Lotus AmiPro Lotus AmiPro dokument Lotus AmiPro-dokumentum Documento Lotus AmiPro Dokumen Lotus AmiPro Documento Lotus AmiPro Lotus AmiPro ドキュメント Lotus AmiPro құжаты Lotus AmiPro 문서 Lotus AmiPro dokumentas Lotus AmiPro dokuments Dokumen Lotus AmiPro Lotus AmiPro-dokument Lotus AmiPro-document Lotus AmiPro-dokument document Lotus AmiPro Dokument Lotus AmiPro documento Lotus AmiPro Documento do Lotus AmiPro Document Lotus AmiPro документ Lotus AmiPro Dokument Lotus AmiPro Dokument Lotus AmiPro Dokument Lotus AmiPro Лотусов Ами Про документ Lotus AmiPro-dokument Lotus AmiPro belgesi документ Lotus AmiPro Tài liệu Lotus AmiPro Lotus AmiPro 文档 Lotus AmiPro 文件 AportisDoc document مستند AportisDoc Документ — AportisDoc document AportisDoc dokument AportisDoc AportisDoc-dokument AportisDoc-Dokument Έγγραφο AportisDoc AportisDoc document AportisDoc-dokumento documento de AportisDoc AportisDoc dokumentua AportisDoc-asiakirja AportisDoc skjal document AportisDoc cáipéis AportisDoc documento de AportiDoc מסמך AportisDoc AportisDoc dokument AportisDoc-dokumentum Documento AportisDoc Dokumen AportisDoc Documento AportisDoc AportisDoc ドキュメント AportisDoc-ის დოკუმენტი AportisDoc құжаты AportisDoc 문서 AportisDoc dokumentas AportisDoc dokuments AportisDoc-document document AportisDoc Dokument AportisDoc documento AportisDoc Documento do AportisDoc Document AportisDoc документ AportisDoc Dokument AportisDoc Dokument AportisDoc Апортис Док документ AportisDoc-dokument AportisDoc belgesi документ AportisDoc Tài liệu AportisDoc AportisDoc 文档 AportisDoc 文件 Applix Spreadsheets spreadsheet جداول بيانات Applix Raźlikovy arkuš Applix Spreadsheets Таблица — Applix Spreadsheets full de càlcul d'Applix Spreadsheets sešit Applix Spreadsheets Applix Spreadsheets-regneark Applix-Spreadsheets-Tabelle Λογιστικό φύλλο Applix Spreadsheets Applix Spreadsheets spreadsheet sterntabelo de Applix Spreadsheets hoja de cálculo de Applix Spreadsheets Applix Spreadsheets kalkulu-orria Applix Spreadsheets -taulukko Applix Spreadsheets rokniark feuille de calcul Applix scarbhileog Applix Spreadsheets folla de cálculo de Applix גליון נתונים של Applix Spreadsheets Applix Spreadsheets proračunska tablica Applix Spreadsheets-munkafüzet Folio de calculo Applix Spreadsheets Lembar sebar Applix Spreadsheets Foglio di calcolo Applix Spreadsheets Applix Spreadsheets スプレッドシート Applix Spreadsheets-ის ცხრილი Applix Spreadsheets электрондық кестесі Applix 스프레드시트 Applix Spreadsheets skaičialentė Applix Spreadsheets izklājlapa Hamparan Applix Spreadsheets Applix Spreadsheets-regneark Applix Spreadsheets-rekenblad Applix Spreadsheets-dokument fuèlh de calcul Applix Arkusz Applix Spreadsheets folha de cálculo Applix Spreadsheets Planilha do Applix Spreadsheets Foaie de calcul Applix электронная таблица Applix Spreadsheets Zošit Applix Spreadsheets Razpredelnica Applix Spreadsheets Fletë llogaritjesh Applix Spreadsheets документ Апликсове Табеле Applix Spreadsheets-kalkylblad Applix Spreadsheets çalışma sayfası ел. таблиця Applix Spreadsheets Bảng tính Applix Spreadsheets Applix Spreadsheets 工作簿 Applix Spreadsheets 試算表 Applix Words document مستند كلمات Applix Applix Words sənədi Dakument Applix Words Документ — Applix Words document d'Applix Words dokument Applix Words Dogfen Applix Words Applix Words-dokument Applix-Words-Dokument Έγγραφο Applix Words Applix Words document dokumento de Applix Words documento de Applix Words Applix Words dokumentua Applix Words -asiakirja Applix Words skjal document Applix Words cáipéis Applix Words documento de Applix Words מסמך של Applix Words Applix Words dokument Applix Words-dokumentum Documento Applix Words Dokumen Applix Words Documento Applix Words Applix Words ドキュメント Applix Words-ის დოკუმენტი Applix Words құжаты Applix Words 문서 Applix Words dokumentas Applix Words dokuments Dokumen Perkataan Applix Applix Words-dokument Applix Words-document Applix Words dokument document Applix Words Dokument Applix Words documento Applix Words Documento do Applix Words Document Applix Words документ Applix Words Dokument Applix Words Dokument Applix Words Dokument Applix Words документ Апликсових Речи Applix Words-dokument Applix Words belgesi документ Applix Words Tài liệu Applix Words Applix Words 文档 Applix Words 文件 ARC archive أرشيف ARC Archiŭ ARC Архив — ARC arxiu ARC archiv ARC ARC-arkiv ARC-Archiv Συμπιεσμένο αρχείο ARC ARC archive ARC-arkivo archivador ARC ARC artxiboa ARC-arkisto ARC skjalasavn archive ARC cartlann ARC arquivo ARC ארכיון ARC ARC arhiva ARC-archívum Archivo ARC Arsip ARC Archivio ARC ARC アーカイブ ARC არქივი ARC архиві ARC 압축 파일 ARC archyvas ARC arhīvs ARC-arkiv ARC-archief ARC-arkiv archiu ARC Archiwum ARC arquivo ARC Pacote ARC Arhivă ARC архив ARC Archív ARC Datoteka arhiva ARC Arkiv ARC АРЦ архива ARC-arkiv ARC arşivi архів ARC Kho nén ARC ARC 归档文件 ARC 封存檔 AR archive أرشيف AR Archiŭ AR Архив — AR arxiu AR archiv AR AR-arkiv AR-Archiv Συμπιεσμένο αρχείο AR AR archive AR-arkivo archivador AR AR artxiboa AR-arkisto AR skjalasavn archive AR cartlann AR arquivo AR ארכיון AR AR arhiva AR-archívum Archivo AR Arsip AR Archivio AR AR アーカイブ AR არქივი AR архиві AR 묶음 파일 AR archyvas AR arhīvs Arkib AR AR-arkiv AR-archief AR-arkiv archiu AR Archiwum AR arquivo AR Pacote AR Arhivă AR архив AR Archív AR Datoteka arhiva AR Arkiv AR АР архива AR-arkiv AR arşivi архів AR Kho nén AR AR 归档文件 AR 封存檔 ARJ archive أرشيف ARJ ARJ arxivi Archiŭ ARJ Архив — ARJ arxiu ARJ archiv ARJ Archif ARJ ARJ-arkiv ARJ-Archiv Συμπιεσμένο αρχείο ARJ ARJ archive ARJ-arkivo archivador ARJ ARJ artxiboa ARJ-arkisto ARJ skjalasavn archive ARJ cartlann ARJ arquivo ARJ ארכיון ARJ ARJ arhiva ARJ-archívum Archivo ARJ Arsip ARJ Archivio ARJ ARJ アーカイブ ARJ არქივი ARJ архиві ARJ 압축 파일 ARJ archyvas ARJ arhīvs Arkib ARJ ARJ-arkiv ARJ-archief ARJ-arkiv archiu ARJ Archiwum ARJ arquivo ARJ Pacote ARJ Arhivă ARJ архив ARJ Archív ARJ Datoteka arhiva ARJ Arkiv ARJ АРЈ архива ARJ-arkiv ARJ arşivi архів ARJ Kho nén ARJ ARJ 归档文件 ARJ 封存檔 ARJ Archived by Robert Jung ASP page صفحة ASP Staronka ASP Страница — ASP pàgina ASP stránka ASP ASP-side ASP-Seite Σελίδα ASP ASP page ASP-paĝo página ASP ASP orria ASP-sivu ASP síða page ASP leathanach ASP páxina ASP עמוד ASP ASP stranica ASP oldal Pagina ASP Halaman ASP Pagina ASP ASP ページ ASP გვერდი ASP парағы ASP 페이지 ASP puslapis ASP lapa ASP-side ASP-pagina ASP-side pagina ASP Strona ASP página ASP Página ASP Pagină ASP страница ASP Stránka ASP Datoteka spletne strani ASP Faqe ASP АСП страница ASP-sida ASP sayfası сторінка ASP Trang ASP ASP 页面 ASP 頁面 ASP Active Server Page AWK script سكربت AWK AWK skripti Skrypt AWK Скрипт — AWK script AWK skript AWK Sgript AWK AWK-program AWK-Skript Δέσμη ενεργειών AWK AWK script AWK-skripto secuencia de órdenes en AWK AWK script-a AWK-komentotiedosto AWK boðrøð script AWK script AWK script de AWK תסריט AWK AWK skripta AWK-parancsfájl Script AWK Skrip AWK Script AWK AWK スクリプト AWK სცენარი AWK сценарийі AWK 스크립트 AWK scenarijus AWK skripts Skrip AWK AWK-skript AWK-script WAK-skript escript AWK Skrypt AWK script AWK Script AWK Script AWK сценарий AWK Skript AWK Skriptna datoteka AWK Script AWK АВК скрипта AWK-skript AWK betiği скрипт AWK Văn lệnh AWK AWK 脚本 AWK 指令稿 BCPIO document مستند BCPIO BCPIO sənədi Dakument BCPIO Документ — BCPIO document BCPIO dokument BCPIO Dogfen BCPIO BCPIO-dokument BCPIO-Dokument Έγγραφο BCPIO BCPIO document BCPIO-dokumento documento BCPIO BCPIO dokumentua BCPIO-asiakirja BCPIO skjal document BCPIO cáipéis BCPIO documento BCPIO מסמך של BCPO BCPIO dokument BCPIO-dokumentum Documento BCPIO Dokumen BCPIO Documento BCPIO BCPIO ドキュメント BCPIO-ის დოკუმენტი BCPIO құжаты BCPIO 문서 BCPIO dokumentas BCPIO dokuments Dokumen BCPIO BCPIO-dokument BCPIO-document BCPIO-dokument document BCPIO Dokument BCPIO documento BCPIO Documento BCPIO Document BCPIO документ BCPIO Dokument BCPIO Dokument BCPIO Dokument BCPIO БЦПИО документ BCPIO-dokument BCPIO belgesi документ BCPIO Tài liệu BCPIO BCPIO 文档 BCPIO 文件 BCPIO Binary CPIO BitTorrent seed file ملف باذر البت تورنت BitTorrent seed faylı Fajł krynicy BitTorrent Файл-източник — BitTorrent fitxer de llavor BitTorrent soubor BitTorrent Ffeil hadu BitTorrent BitTorrent-frøfil BitTorrent-Seed-Datei Αρχείο BitTorrent seed BitTorrent seed file BitTorrent-semdosiero archivo semilla de BitTorrent BitTorrent hazi-fitxategia BitTorrent-siementiedosto BitTorrent seed fíla fichier graine BitTorrent comhad síl BitTorrent ficheiro de orixe BitTorrent קובץ זריעה של BitTorrent BitTorrent datoteka BitTorrent-magfájl File seminal de BitTorrent Berkas benih BitTorrent File seed BitTorrent BitTorrent シードファイル BitTorrent көз файлы 비트토렌트 시드 파일 BitTorrent šaltinio failas BitTorrent avota datne Fail seed BitTorrent Fil med utgangsverdi for BitTorrent BitTorrent-bestand Nedlastingsfil for BitTorrent fichièr grana BitTorrent Plik ziarna BitTorrent ficheiro de semente BitTorrent Arquivo semente BitTorrent Fișier sursă-completă BitTorrent файл источника BitTorrent Súbor BitTorrent Datoteka sejanja BitTorrent File bazë BitTorrent датотека сејача Бит Торента BitTorrent-distributionsfil BitTorrent tohum dosyası файл поширення BitTorrent Tải tập hạt BitTorrent BitTorrent 种子文件 BitTorrent 種子檔 Blender scene مشهد بلندر Scena Blender Сцена — Blender escena de Blender scéna Blender Blenderscene Blender-Szene Σκηνή Blender Blender scene Blender-sceno escena de Blender Blender-eko fitxategia Blender-näkymä Blender leikmynd scène Blender radharc Blender escena de Blender סצנת Blender Blender scena Blender-jelenet Scena Blender Scene Blender Scena Blender Blender シーン Blender-ის სცენა Blender сахнасы Blender 장면 Blender scena Blender aina Babak Blender Blender-scene Blender-scène Blender-scene scèna Blender Scena programu Blender cenário Blender Cena do Blender Scenă Blender сцена Blender Scéna Blender Datoteka scene Blender Skenë Blender Блендерова сцена Blender-scen Blender sahnesi сцена Blender Cảnh Blender Blender 场景 Blender 場景 TeX DVI document (bzip-compressed) مستند TeX DVI (مضغوط-bzip) Dakument TeX DVI (bzip-skampresavany) Документ — TeX DVI, компресиран с bzip document de TeX DVI (amb compressió bzip) dokument TeX DVI (komprimovaný pomocí bzip) TeX DVI-dokument (bzip-komprimeret) TeX-DVI-Dokument (bzip-komprimiert) Αρχείο TeX DVI (συμπιεσμένο με bzip) TeX DVI document (bzip-compressed) documento DVI de TeX (comprimido con bzip) TeX DVI dokumentua (bzip-ekin konprimitua) TeX DVI -asiakirja (bzip-pakattu) TeX DVI skjal (bzip-stappað) document DVI TeX (compressé bzip) cáipéis DVI TeX (comhbhrúite le bzip) documento DVI de TeX (comprimido con bzip) מסמך מסוג TeX DVI (מכווץ ע״י bzip) TeX DVI dokument (komprimiran bzip-om) TeX DVI dokumentum (bzip-pel tömörítve) Documento TeX DVI (comprimite con bzip) Dokumen TeX DVI (terkompresi bzip) Documento TeX DVI (compresso con bzip) Tex DVI ドキュメント (bzip 圧縮) TeX DVI құжаты (bzip-пен сығылған) TeX DVI 문서(BZIP 압축) TeX DVI dokumentas (suglaudintas su bzip) TeX DVI dokuments (saspiests ar bzip) TeX DVI-dokument (bzip-komprimert) TeX DVI-document (ingepakt met bzip) TeX DVI-dokument (pakka med bzip) document DVI TeX (compressat bzip) Dokument TeX DVI (kompresja bzip) documento TeX DVI (compressão bzip) Documento DVI TeX (compactado com bzip) Document TeX DVI (comprimat bzip) документ TeX DVI (сжатый bzip) Dokument TeX DVI (komprimovaný pomocou bzip) Dokument TeX DVI (stisnjen z bzip) Dokument Tex DVI (i kompresuar me bzip) ТеКс ДВИ документ (запакована бзипом) TeX DVI-dokument (bzip-komprimerat) TeX DVI belgesi (bzip ile sıkıştırılmış) документ TeX DVI (стиснений bzip) Tài liệu DVI TeX (đã nén bzip) TeX DVI 文档(gzip 压缩) TeX DVI 文件 (bzip 格式壓縮) Bzip archive أرشيف Bzip Archiŭ bzip Архив — bzip arxiu bzip archiv bzip Bzip-arkiv Bzip-Archiv Συμπιεσμένο αρχείο Bzip Bzip archive Bzip-arkivo archivador Bzip Bzip artxiboa Bzip-arkisto Bzip skjalasavn archive bzip cartlann Bzip arquivo Bzip ארכיון Bzip Bzip arhiva Bzip archívum Archivo Bzip Arsip Bzip Archivio bzip Bzip アーカイブ Bzip არქივი Bzip архиві BZIP 압축 파일 Bzip archyvas Bzip arhīvs Bzip-arkiv Bzip-archief Bzip-arkiv archiu bzip Archiwum bzip arquivo Bzip Pacote Bzip Arhivă Bzip архив BZIP Archív bzip Datoteka arhiva Bzip Arkiv bzip Бзип архива Bzip-arkiv Bzip arşivi архів bzip Kho nén bzip bzip 归档文件 Bzip 封存檔 Tar archive (bzip-compressed) أرشيف Tar (مضغوط-bzip) Archiŭ tar (bzip-skampresavany) Архив — tar, компресиран с bzip arxiu tar (amb compressió bzip) archiv Tar (komprimovaný pomocí bzip) Tar-arkiv (bzip-komprimeret) Tar-Archiv (bzip-komprimiert) Αρχείο Tar (συμπιεσμένο με bzip) Tar archive (bzip-compressed) archivador Tar (comprimido con bzip) Tar artxiboa (bzip-ekin konprimitua) Tar-arkisto (bzip-pakattu) Tar skjalasavn (bzip-stappað) archive tar (compressée bzip) cartlann Tar (comhbhrúite le bzip) arquivo Tar (comprimido con bzip) ארכיון Tar (מכווץ ע״י bzip) Tar arhiva (komprimirana bzip-om) Tar archívum (bzip-pel tömörítve) Archivo Tar (comprimite con bzip) Arsip Tar (terkompresi bzip) Archivio tar (compresso con bzip) Tar アーカイブ (bzip 圧縮) Tar архиві (bzip-пен сығылған) TAR 묶음 파일(BZIP 압축) Tar archyvas (suglaudintas su bzip) Tar arhīvs (saspiests ar bzip) Tar-arkiv (bzip-komprimert) Tar-archief (ingepakt met bzip) Tar-arkiv (pakka med bzip) archiu tar (compressat bzip) Archiwum tar (kompresja bzip) arquivo Tar (compressão bzip) Pacote Tar (compactado com bzip) Arhivă Tar (comprimată bzip) архив TAR (сжатый BZIP) Archív tar (komprimovaný pomocou bzip) Datoteka arhiva Tar (stisnjen z bzip) Arkiv tar (i kompresuar me bzip) Тар архива (запакована бзипом) Tar-arkiv (bzip-komprimerat) Tar arşivi (bzip ile sıkıştırılmış) архів tar (стиснений bzip) Kho nén tar (đã nén bzip) Tar 归档文件(bzip 压缩) Tar 封存檔 (bzip 格式壓縮) PDF document (bzip-compressed) مستند PDF (مضغوط-bzip) Dakument PDF (bzip-skampresavany) Документ — PDF, компресиран с bzip document PDF (amb compressió bzip) dokument PDF (komprimovaný pomocí bzip) PDF-dokument (bzip-komprimeret) PDF-Dokument (bzip-komprimiert) Έγγραφο PDF (συμπιεσμένο με bzip) PDF document (bzip-compressed) documento PDF (comprimido con bzip) PostScript dokumentua (bzip-ekin konprimitua) PDF-asiakirja (bzip-pakattu) PDF skjal (bzip-stappað) document PDF (compressé bzip) cáipéis PDF (comhbhrúite le bzip) documento PDF (comprimido en bzip) מסמך PDF (מכווץ ע״י bzip) PDF dokument (bzip sažet) PDF dokumentum (bzip-tömörítésű) Documento PDF (comprimite con bzip) Dokumen PDF (terkompresi bzip) Documento PDF (compresso con bzip) PDF ドキュメント (bzip 圧縮) PDF құжаты (bzip-пен сығылған) PDF 문서(BZIP 압축) PDF dokumentas (suglaudintas su bzip) PDF dokuments (saspiests ar bzip) PDF-dokument (bzip-komprimert) PDF-document (ingepakt met bzip) PDF-dokument (pakka med bzip) document PDF (compressat bzip) Dokument PDF (kompresja bzip) documento PDF (compressão bzip) Documento PDF (compactado com bzip) Document PDF (comprimat bzip) документ PDF (сжатый bzip) Dokument PDF (komprimovaný pomocou bzip) Dokument PDF (stisnjen z bzip) Dokument PDF (i kompresuar me bzip) ПДФ документ (запакован бзипом) PDF-dokument (bzip-komprimerat) PDF belgesi (bzip ile sıkıştırılmış) документ PDF (стиснений bzip) Tài liệu PDF (đã nén bzip) PDF 文档(bzip 压缩) PDF 文件 (bzip 格式壓縮) PostScript document (bzip-compressed) مستند PostScript (مضغوط-bzip) Dakument PostScript (bzip-skampresavany) Документ — PostScript, компресиран с bzip document PostScript (amb compressió bzip) dokument PostScript (komprimovaný pomocí bzip) PostScript-dokument (bzip-komprimeret) PostScript-Dokument (bzip-komprimiert) Έγγραφο PostScript (συμπιεσμένο με bzip) PostScript document (bzip-compressed) documento PostScript (comprimido con bzip) PostScript dokumentua (bzip-ekin konprimitua) PostScript-asiakirja (bzip-pakattu) PostScript skjal (bzip-stappað) document PostScript (compressé bzip) cáipéis PostScript (comhbhrúite le bzip) documento PostScript (comprimido con bzip) מסמך PostDcript (מכווץ ע״י bzip) PostScript dokument (bzip sažet) PostScript dokumentum (bzip-tömörítésű) Documento PostScript (comprimite con bzip) Dokumen PostScript (terkompresi bzip) Documento PostScript (compresso con bzip) PostScript ドキュメント (bzip 圧縮) PostScript құжаты (bzip-пен сығылған) PostScript 문서(BZIP 압축) PostScript dokumentas (suglaudintas su bzip) PostScript dokuments (saspiests ar bzip) PostScript-dokument (bzip-komprimert) PostScript-document (ingepakt met bzip) PostScript-dokument (pakka med bzip) document PostEscript (compressat bzip) Dokument Postscript (kompresja bzip) documento PostScript (compressão bzip) Documento PostScript (compactado com bzip) Document PostScript (comprimat bzip) документ PostScript (сжатый bzip) Dokument PostScript (komprimovaný pomocou bzip) Dokument PostScript (stisnjen z bzip) Dokument PostScript (i kompresuar me bzip) Постскрипт документ (запакован бзипом) Postscript-dokument (bzip-komprimerat) PostScript belgesi (bzip ile sıkıştırılmış) документ PostScript (стиснене bzip) Tài liệu PostScript (đã nén bzip) PostScript 文档(bzip 压缩) PostScript 文件 (bzip 格式壓縮) comic book archive أرشيف comic book archiŭ komiksaŭ Архив — комикси arxiu comic book archiv knihy komiksů comic book-arkiv Comic-Book-Archiv Συμπιεσμένο αρχείο κόμικ comic book archive archivador de libro de cómic komiki artxiboa sarjakuva-arkisto teknisøgubóka skjalasavn archive Comic Book cartlann chartúin ficheiro de libro de banda deseñada ארכיון ספר קומי Strip arhiva képregényarchívum Archivo Comic Book arsip buku komik Archivio comic book コミックブックアーカイブ комикстар архиві 만화책 압축 파일 komiksų knygos archyvas komiksu grāmatas arhīvs Tegneseriearkiv stripboek-archief teikneseriearkiv archiu Comic Book Archiwum komiksu arquivo de banda desenhada Pacote de histórias em quadrinhos arhivă benzi desenate архив комиксов Archív knihy komiksov Datoteka arhiva stripov Arkiv comic book архива стрипа serietidningsarkiv çizgi roman arşivi архів коміксів Kho nén sách tranh chuyện vui Comic Book 归档文件 漫畫書封存檔 comic book archive أرشيف comic book archiŭ komiksaŭ Архив — комикси arxiu comic book archiv knihy komiksů comic book-arkiv Comic-Book-Archiv Συμπιεσμένο αρχείο κόμικ comic book archive archivador de libro de cómic komiki artxiboa sarjakuva-arkisto teknisøgubóka skjalasavn archive Comic Book cartlann chartúin ficheiro de libro de banda deseñada ארכיון ספר קומי Strip arhiva képregényarchívum Archivo Comic Book arsip buku komik Archivio comic book コミックブックアーカイブ комикстар архиві 만화책 압축 파일 komiksų knygos archyvas komiksu grāmatas arhīvs Tegneseriearkiv stripboek-archief teikneseriearkiv archiu Comic Book Archiwum komiksu arquivo de banda desenhada Pacote de histórias em quadrinhos arhivă benzi desenate архив комиксов Archív knihy komiksov Datoteka arhiva stripov Arkiv comic book архива стрипа serietidningsarkiv çizgi roman arşivi архів коміксів Kho nén sách tranh chuyện vui Comic Book 归档文件 漫畫書封存檔 comic book archive أرشيف comic book archiŭ komiksaŭ Архив — комикси arxiu comic book archiv knihy komiksů comic book-arkiv Comic-Book-Archiv Συμπιεσμένο αρχείο κόμικ comic book archive archivador de libro de cómic komiki artxiboa sarjakuva-arkisto teknisøgubóka skjalasavn archive Comic Book cartlann chartúin ficheiro de libro de banda deseñada ארכיון ספר קומי Strip arhiva képregényarchívum Archivo Comic Book arsip buku komik Archivio comic book コミックブックアーカイブ комикстар архиві 만화책 압축 파일 komiksų knygos archyvas komiksu grāmatas arhīvs Tegneseriearkiv stripboek-archief teikneseriearkiv archiu Comic Book Archiwum komiksu arquivo de banda desenhada Pacote de histórias em quadrinhos arhivă benzi desenate архив комиксов Archív knihy komiksov Datoteka arhiva stripov Arkiv comic book архива стрипа serietidningsarkiv çizgi roman arşivi архів коміксів Kho nén sách tranh chuyện vui Comic Book 归档文件 漫畫書封存檔 comic book archive أرشيف comic book archiŭ komiksaŭ Архив — комикси arxiu comic book archiv knihy komiksů comic book-arkiv Comic-Book-Archiv Συμπιεσμένο αρχείο κόμικ comic book archive archivador de libro de cómic komiki artxiboa sarjakuva-arkisto teknisøgubóka skjalasavn archive Comic Book cartlann chartúin ficheiro de libro de banda deseñada ארכיון ספר קומי Strip arhiva képregényarchívum Archivo Comic Book arsip buku komik Archivio comic book コミックブックアーカイブ комикстар архиві 만화책 압축 파일 komiksų knygos archyvas komiksu grāmatas arhīvs Tegneseriearkiv stripboek-archief teikneseriearkiv archiu Comic Book Archiwum komiksu arquivo de banda desenhada Pacote de histórias em quadrinhos arhivă benzi desenate архив комиксов Archív knihy komiksov Datoteka arhiva stripov Arkiv comic book архива стрипа serietidningsarkiv çizgi roman arşivi архів коміксів Kho nén sách tranh chuyện vui Comic Book 归档文件 漫畫書封存檔 Lrzip archive أرشيف Lrzip Архив — lrzip arxiu lrzip archiv Lrzip Lrzip-arkiv Lrzip-Archiv Συμπιεσμένο αρχείο Lrzip Lrzip archive Lrzip-arkivo archivador Lrzip Lrzip artxiboa Lrzip-arkisto Lrzip skjalasavn archive lrzip cartlann Lrzip arquivo Lrzip ארכיון Lrzip Lrzip arhiva Lrzip archívum Archivo Lrzip Arsip Lrzip Archivio Lrzip Lrzip アーカイブ Lrzip архиві LRZIP 압축 파일 Lrzip archyvas Lrzip arhīvs Lrzip archief archiu lrzip Archiwum lrzip arquivo Lrzip Pacote Lrzip Arhivă Lrzip архив LRZIP Archív Lrzip Datoteka arhiva Lrzip Лрзип архива Lrzip-arkiv Lrzip arşivi архів lrzip Lrzip 归档文件 Lrzip 封存檔 Tar archive (lrzip-compressed) أرشيف Tar (مضغوط-lrzip) Архив — tar, компресиран с lrzip arxiu tar (amb compressió lrzip) archiv Tar (komprimovaný pomocí lrzip) Tar-arkiv (lrzip-komprimeret) Tar-Archiv (lrzip-komprimiert) Αρχείο Tar (συμπιεσμένο με lrzip) Tar archive (lrzip-compressed) archivador Tar (comprimido con lrzip) Tar artxiboa (lrzip-ekin konprimitua) Tar-arkisto (lrzip-pakattu) Tar skjalasavn (lrzip-stappað) archive tar (compressée lrzip) cartlann Tar (comhbhrúite le lrzip) arquivo Tar (comprimido con lrzip) ארכיון Tar (מכווץ ע״י lrzip) Tar arhiva (komprimirana lrzip-om) Tar archívum (lrzip-pel tömörítve) Archivo Tar (comprimite con lrzip) Arsip Tar (terkompresi lrzip) Archivio tar (compresso con lrzip) Tar アーカイブ (lrzip 圧縮) Tar архиві (lrzip-пен сығылған) TAR 묶음 파일(LRZIP 압축) Tar archyvas (suglaudintas su lrzip) Tar arhīvs (saspiests ar lrzip) Tar archief (lrzip-compressed) archiu tar (compressat lrzip) Archiwum tar (kompresja lrzip) arquivo Tar (compressão Lrzip) Pacote Tar (compactado com lrzip) Arhivă Tar (comprimată lrzip) архив TAR (сжатый LRZIP) Archív tar (komprimovaný pomocou lrzip) Datoteka arhiva Tar (stisnjen z lrzip) Тар архива (запакована лрзипом) Tar-arkiv (lrzip-komprimerat) Tar arşivi (lrzip ile sıkıştırılmış) архів tar (стиснений lrzip) Tar 归档文件 (lrzip 压缩) Tar 封存檔 (lrzip 格式壓縮) Apple disk image Диск — Apple imatge de disc d'Apple obraz disku Apple Apple-diskaftryk Apple-Datenträgerabbild Εικόνα δίσκου Apple Apple disk image imagen de disco de Apple Apple disko irudia Apple-levytiedosto image disque Apple imaxe de disco de Appl תמונת כונן Apple Apple snimka diska Apple lemezkép Imagine de disco Apple Image disk Apple Immagine disco Apple Apple ディスクイメージ Apple-ის სადისკო გამოსახულება Apple диск бейнесі Apple 디스크 이미지 Apple diska attēls Apple disk image imatge disc Apple Obraz dysku Apple imagem de disco Apple Imagem de disco Apple образ диска Apple Mac OS X Obraz disku Apple Odtis diska Apple Еплова слика диска Apple-diskavbild Apple disk görüntüsü образ диска Apple Apple 磁盘镜像 Apple 磁碟映像 Raw disk image imatge de disc RAW surový obraz disku Rå diskaftryk Rohes Datenträgerabbild Ανεπεξέργαστη εικόνα δίσκου Raw disk image imagen de disco en bruto Disko gordinaren irudia Raaka levytiedosto image disque Raw Imaxe de disco en bruto דמות גולמית של כונן Osnovna slika diska Nyers lemezkép Imagine de disco crude Image disk mentah Immagine disco raw Шикі диск бейнесі RAW 디스크 이미지 imatge disc Raw Surowy obraz dysku imagem de disco Raw Imagem bruta de disco необработанный образ диска Obraz disku Surovi odtis diska сирова слика диска Rå diskavbild İşlem görmemiş disk imajı простий образ диска 原始磁盘镜像 原生磁碟映像 Raw disk image (XZ-compressed) imatge de disc RAW (amb compressió XZ) surový obraz disku (komprimovaný pomocí XZ) Rå diskaftryk (XZ-komprimeret) Rohes Datenträgerabbild (XZ-komprimiert) Ανεπεξέργαστη εικόνα δίσκου (συμπιεσμένη XZ) Raw disk image (XZ-compressed) imagen de disco en bruto (comprimida con XZ) Disko gordinaren irudia (XZ-rekin konprimitua) Raaka levytiedosto (XZ-pakattu) image disque Raw (compression XZ) Imaxe de disco en bruto (comprimida en XZ) דמות גולמית של כונן (בדחיסת XZ) Osnovna slika diska (XZ sažeta) Nyers lemezkép (XZ-vel tömörítve) Imagine de disco crude (comprimite con XZ) Image disk mentah (terkompresi XZ) Immagine disco raw (compressa XZ) Шикі диск бейнесі (XZ-мен сығылған) RAW 디스크 이미지(XZ 압축) imatge disc Raw (compression XZ) Surowy obraz dysku (kompresja XZ) imagem de disco Raw (compressão XZ) Imagem bruta de disco (compactada com XZ) необработанный образ диска (XZ-сжатый) Obraz disku (komprimovaný pomocou XZ) Surovi odtis diska (stisnjeno z XZ) сирова слика диска (запакована ИксЗ-ом) Rå diskavbild (XZ-komprimerad) İşlem görmemiş disk imajı (XZ ile sıkıştırılmış) простий образ диска (стиснений XZ) 原始磁盘镜像(XZ 压缩) 原生磁碟映像 (XZ 格式壓縮) raw CD image صورة CD خامة suvoraja vyjava CD Изображение — raw CD imatge de CD en cru surový obraz CD rå cd-aftryk CD-Roh-Abbild Εικόνα περιεχομένου ψηφιακού δίσκου raw CD image kruda lumdiskbildo imagen de CD en bruto CD gordinaren irudia raaka CD-vedos rá CD mynd image CD brute amhíomhá dhlúthdhiosca imaxe de CD en bruto תמונת דיסק גולמית Osnovna CD slika nyers CD-lemezkép Imagine CD brute citra CD mentah Immagine raw CD 生 CD イメージ өңделмеген CD бейнесі CD 이미지 raw CD atvaizdis CD jēlattēls Imej CD mentah rått CD-bilde ruw CD-beeldbestand rått CD-bilete imatge CD brut Surowy obraz CD imagem em bruto de CD Imagem bruta de CD imagine de CD brută необработанный образ компакт-диска Surový obraz CD surovi CD odtis Imazh raw CD сирова слика ЦД-а rå cd-avbild Ham CD görüntüsü образ raw CD ảnh đĩa CD thô 原始 CD 映像 原生 CD 映像 AppImage application bundle CD Table Of Contents جدول محتويات الـ CD Źmieściva CD Съдържание на CD taula de continguts de CD obsah CD Cd-indholdsfotegnelse CD-Inhaltsverzeichnis Πίνακας περιεχομένων CD CD Table Of Contents índice de contenido de CD CDaren edukien aurkibidea CD-sisällysluettelo CD innihaldsyvurlit table des matières de CD clár ábhar dlúthdhiosca táboa de contidos de CD תוכן עניינים של דיסק CD sadržaj CD tartalomjegyzék Tabula de contento de CD Tabel Isi CD Indice CD CD Table Of Contents CD құрама кестесі CD 내용 목록 CD turinys CD satura rādītājs Innholdsfortegnelse for CD CD-inhoudsopgave CD innhaldsliste ensenhador de CD Plik zawartości płyty CD Tabela de conteúdos de CD Sumário de CD Tabel conținut CD таблица содержания CD Obsah CD Kazalo vsebine CD nosilca Tregues CD табела садржаја ЦД-а Cd-innehållsförteckning CD İçindekiler Tablosu зміст CD Mục Lục của đĩa CD CD 索引 CD 內容目錄 PGN chess game notation تدوينة لعبة الشطرنج PGN Zaciem ab šachmatnaj partyi PGN Игра шах — PGN notació de joc d'escacs PGN šachová notace PGN PGN-skakspilsnotation PGN-Schachspielnotation Σημειογραφία παιχνιδιού σκακιού PGN PGN chess game notation notación para juegos de ajedrez PGN PGN xake jokoaren notazioa PGN-šakkipelinotaatio PGN talv teknskipan notation de jeu d'échecs PGN nodaireacht chluiche ficheall PGN Notación de xogo de xadrez PGN סימון משחק שח PGN PGN zapis šahovske igre PGN sakkfeljegyzés Notation de joco de chacos PGN Notasi permainan catur PGN Notazione partita a scacchi PGN PGN チェスゲーム記録 PGN шахмат ойыны PGN 체스 게임 기보 PGN šachmatų žaidimo žymėjimas PGN šaha spēles notācija PGN sjakkspillnotasjon PGN-schaakspelnotatie PGN-sjakkspelnotasjon notacion de jòc d'escacs PGN Plik PGN notacji gry w szachy notação de jogo de xadrez PGN Notação de jogo de xadrez PGN Notație joc șah PGN шахматная партия PGN Šachová notácia PGN Datoteka opomb šahovske igre PGN Njoftim loje shahu PGN ПГН забелешка шаховске игре PGN-schackpartinotation PGN satranç oyun gösterimi запис гри у шахи PGN Cách ghi lượt chơi cờ PGN PGN 象棋游戏注记 PGN 國際象棋棋譜 PGN Portable Game Notation CHM document مستند CHM Dakument CHM Документ — CHM document CHM dokument CHM CHM-dokument CHM-Dokument Έγγραφο CHM CHM document CHM-dokumento documento CHM CHM dokumentua CHM-asiakirja CHM skjal document CHM cáipéis CHM documento CHM מסמך CHM CHM dokument CHM dokumentum Documento CHM Dokumen CHM Documento CHM CHM ドキュメント CHM დოკუმენტი CHM құжаты CHM 문서 CHM dokumentas CHM dokuments CHM-dokument CHM-document CHM-dokument document CHM Dokument CHM documento CHM Documento CHM Document CHM документ CHM Dokument CHM Dokument CHM Dokument CHM ЦХМ документ CHM-dokument CHM belgesi документ CHM Tài liệu CHM CHM 文档 CHM 文件 CHM Compiled Help Modules Java byte code رمز بايت الـJava Java bayt kodu Bajtavy kod Java Байт код за Java Bytecode de Java bajtový kód Java Côd beit Java Javabytekode Java-Bytecode Συμβολοκώδικας Java Java byte code Java-bajtkodo bytecode de Java Java byte-kodea Java-tavukoodi Java býtkota code Java binaire beartchód Java byte code de Java קוד Java byte Java bajt kôd Java-bájtkód Codice intermediari de Java Kode bita Java Bytecode Java Java バイトコード Java байт коды Java 바이트 코드 Java baitinis kodas Java bitu kods Kod bait Java Java-bytekode Java-bytecode Jave byte-kode còde Java binari Kod bajtowy Java byte-code Java Código compilado Java Bytecode Java байт-код Java Bajtový kód Java Datoteka bitne kode Java Byte code Java бајтни ко̂д Јаве Java-bytekod Java derlenmiş kodu Байт-код Java Mã byte Java Java 字节码 Java 位元組碼 UNIX-compressed file ملف يونكس-مضغوط Skampresavany UNIX-fajł Файл — компресиран за UNIX fitxer amb compressió UNIX soubor komprimovaný v Unixu UNIX-komprimeret fil UNIX-komprimierte Datei Συμπιεσμένο αρχείο UNIX UNIX-compressed file UNIX-kunpremita dosiero archivo comprimido de Unix UNIX-en konprimitutako fitxategia UNIX-pakattu tiedosto UNIX-stappað fíla fichier compressé UNIX comhad UNIX-comhbhrúite ficheiro comprimido de UNIX קובץ מכווץ של UNIX UNIX-komprimirana datoteka Tömörített UNIX-fájl File comprimite de UNIX Berkas terkompresi UNIX File compresso-UNIX UNIX-compress ファイル файл (UNIX-сығылған) UNIX 압축 파일 UNIX suglaudintas failas UNIX saspiesta datne Fail termampat-UNIX UNIX-komprimert fil UNIX-ingepakt bestand UNIX-komprimert fil fichièr compressat UNIX Skompresowany plik systemu UNIX ficheiro comprimido UNIX Arquivo compactado do UNIX Fișier comprimat UNIX файл (UNIX-сжатый) Súbor komprimovaný v Unixe Skrčena Unix datoteka File i kompresuar UNIX датотека запакована ЈУНИКС-ом UNIX-komprimerad fil UNIX-sıkıştırılmış dosyası стиснений файл UNIX Tập tin đã nén UNIX UNIX 压缩文件 UNIX 格式壓縮檔 Tar archive (gzip-compressed) أرشيف Tar (مضغوط-gzip) Archiŭ tar (gzip-skampresavany) Архив — tar, компресиран с gzip arxiu tar (amb compressió gzip) archiv tar (komprimovaný pomocí gzip) Tar-arkiv (gzip-komprimeret) Tar-Archiv (gzip-komprimiert) Αρχείο Tar (συμπιεσμένο με gzip) Tar archive (gzip-compressed) archivador Tar (comprimido con gzip) Tar artxiboa (gzip-ekin konprimitua) Tar-arkisto (gzip-pakattu) Tar skjalasavn (gzip-stappað) archive tar (compressée gzip) cartlann Tar (comhbhrúite le gzip) arquivo Tar (comprimido con gzip) ארכיון Tar (מכווץ ע״י gzip) Tar arhiva (komprimirana gzip-om) Tar archívum (gzip-pel tömörítve) Archivo Tar (comprimite con gzip) Arsip Tar (terkompresi gzip) Archivio tar (compresso con gzip) Tar アーカイブ (gzip 圧縮) Tar архиві (gzip-пен сығылған) TAR 묶음 파일(GZIP 압축) Tar archyvas (suglaudintas su gzip) Tar arhīvs (saspiests ar gzip) Tar-arkiv (gzip-komprimert) Tar-archief (ingepakt met gzip) Tar-arkiv (pakka med gzip) archiu tar (compressat gzip) Archiwum tar (kompresja gzip) arquivo Tar (compressão gzip) Pacote Tar (compactado com gzip) Arhivă Tar (comprimată gzip) архив TAR (сжатый GZIP) Archív tar (komprimovaný pomocou gzip) Datoteka arhiva Tar (stisnjen z gzip) Arkiv tar (i kompresuar me gzip) Тар архива (запакована гзипом) Tar-arkiv (gzip-komprimerat) Tar arşivi (gzip ile sıkıştırılmış) архів tar (стиснений gzip) Kho nén tar (đã nén gzip) Tar 归档文件(gzip 压缩) Tar 封存檔 (gzip 格式壓縮) program crash data معلومات انهيار البرنامج źviestki złamanaj prahramy Данни от забиване на програма dades de fallada de programa data o pádu programu programnedbrudsdata Daten zu Programmabsturz δεδομένα από την κατάρρευση προγράμματος program crash data datumo pri kraŝo de programo datos de cuelgue de programa programaren kraskaduraren datuak ohjelman kaatumistiedot forrits sordáta données de plantage de programme sonraí thuairt ríomhchláir datos de colgue do programa מידע מקריסת תוכנה podaci o rušenju programa összeomlott program adatai Datos de fallimento de programma data program macet Dati crash di applicazione プログラムクラッシュデータ апатты аяқтаудың мәліметтері 프로그램 비정상 종료 데이터 programos nulūžimo duomenys programmas avārijas dati Data program musnah krasjdata fra program programma-crashgegevens data om programkrasj donadas de plantage de programa Dane awarii programu dados de rebentamento de aplicação Dados de travamento de programa date eroare program данные аварийного завершения Údaje o páde programu podatki sesutja programa Të dhëna nga programi i bllokuar подаци о падовима програма programkraschdata program çökme verisi аварійні дані про програму dữ liệu sụp đổ chương trình 程序崩溃数据 程式當掉資料 CPIO archive أرشيف CPIO CPIO arxivi Archiŭ CPIO Архив — CPIO arxiu CPIO archiv CPIO Archif CPIO CPIO-arkiv CPIO-Archiv Συμπιεσμένο αρχείο CPIO CPIO archive CPIO-arkivo archivador CPIO CPIO artxiboa CPIO-arkisto CPIO skjalasavn archive CPIO cartlann CPIO arquivo CPIO ארכיון CPIO CPIO arhiva CPIO-archívum Archivo CPIO Arsip CPIO Archivio CPIO CPIO アーカイブ CPIO არქივი CPIO архиві CPIO 묶음 파일 CPIO archyvas CPIO arhīvs Arkib CPIO CPIO-arkiv CPIO-archief CPIO-arkiv archiu CPIO Archiwum CPIO arquivo CPIO Pacote CPIO Arhivă CPIO архив CPIO Archív CPIO Datoteka arhiva CPIO Arkiv CPIO ЦПИО архива CPIO-arkiv CPIO arşivi архів CPIO Kho nén CPIO CPIO 归档文件 CPIO 封存檔 CPIO archive (gzip-compressed) أرشيف CPIO (مضغوط-gzip) CPIO arxivi (gzip ilə sıxışdırılmış) Archiŭ CPIO (gzip-skampresavany) Архив — CPIO, компресиран с gzip arxiu CPIO (amb compressió gzip) archiv CPIO (komprimovaný pomocí gzip) Archif CPIO (gywasgwyd drwy gzip) CPIO-arkiv (gzip-komprimeret) CPIO-Archiv (gzip-komprimiert) Αρχείο CPIO (συμπιεσμένο με gzip) CPIO archive (gzip-compressed) CPIO-arkivo (kunpremita per gzip) archivador CPIO (comprimido con gzip) CPIO artxiboa (gzip-ekin konprimitua) CPIO-arkisto (gzip-pakattu) CPIO skjalasavn (gzip-stappað) archive CPIO (compressé gzip) cartlann CPIO (comhbhrúite le gzip) arquivo CPIO (comprimido con gzip) ארכיון CPIO (מכווץ ע״י gzip) CPIO arhiva (komprimirana gzip-om) CPIO-archívum (gzip-pel tömörítve) Archivo CPIO (comprimite con gzip) Arsip CPIO (terkompresi gzip) Archivio CPIO (compresso con gzip) CPIO (gzip 圧縮) アーカイブ CPIO არქივი (gzip-ით შეკუმშული) CPIO архиві (gzip-пен сығылған) CPIO 묶음 파일(GZIP 압축) CPIO archyvas (suglaudintas su gzip) CPIO arhīvs (saspiests ar gzip) Arkib CPIO (dimampatkan-gzip) CPIO-arkiv (gzip-komprimert) CPIO-archief (ingepakt met gzip) CPIO-arkiv (gzip-pakka) archiu CPIO (compressat gzip) Archiwum CPIO (kompresja gzip) arquivo CPIO (compressão gzip) Pacote CPIO (compactado com gzip) Arhivă CPIO (compresie gzip) архив CPIO (сжатый GZIP) Archív CPIO (komprimovaný pomocou gzip) Datoteka arhiva CPIO (skrčena z gzip) Arkiv CPIO (kompresuar me gzip) ЦПИО архива (компресована гзип-ом) CPIO-arkiv (gzip-komprimerat) CPIO arşivi (gzip ile sıkıştırılmış) архів CPIO (стиснений gzip) Kho nén CPIO (đã nén gzip) CPIO 归档文件(gzip 压缩) CPIO 封存檔 (gzip 格式壓縮) C shell script سكربت شِل سي C qabıq skripti Skrypt abałonki C Скрипт — обвивка C script de C shell skript shellu C Sgript plisgyn C C-skalprogram C-Shell-Skript Δέσμη ενεργειών κελύφους C C shell script skripto de C-ŝelo secuencia de órdenes de consola en C C shell script-a Csh-komentotiedosto C skel boðrøð script C shell script bhlaosc C script de C shell תסריט מעטפת C C skripta C héj-parancsfájl Script C-shell Skrip shell C Script C shell C シェルスクリプト C shell сценарийі C 셸 스크립트 C shell scenarijus C čaulas skripts Skrip shell C C-skallskript C-shellscript C-skalskript escript C shell Skrypt powłoki C script de terminal C Script de shell C Script C shell сценарий C shell Skript shellu C Skriptna datoteka lupine C Script shell C скрипта Ц љуске Skalskript (csh) C kabuk betiği скрипт оболонки C Văn lệnh trình bao C C shell 脚本 C shell 指令稿 Xbase document مستند Xbase Dakument Xbase Документ — Xbase document Xbase dokument Xbase Xbasedokument Xbase-Dokument Έγγραφο Xbase Xbase document Xbase-dokumento documento Xbase Xbase dokumentua Xbase-asiakirja Xbase skjal document Xbase cáipéis Xbase documento Xbase מסמך Xbase Xbase dokumenet Xbase dokumentum Documento Xbase Dokumen Xbase Documento Xbase Xbase ドキュメント Xbase құжаты Xbase 문서 Xbase dokumentas Xbase dokuments Xbase-dokument Xbase-document Xbase-dokument document Xbase Dokument Xbase documento Xbase Documento do Xbase Document Xbase документ Xbase Dokument Xbase Dokument Xbase Dokument Xbase документ Иксбазе Xbase-dokument Xbase belgesi документ Xbase Tài liệu Xbase Xbase 文档 Xbase 文件 ECMAScript program برنامج ECMAScript Prahrama ECMAScript Програма — ECMAScript programa ECMAScript program v jazyce ECMAScript ECMA-program ECMAScript-Programm Πρόγραμμα ECMAScript ECMAScript program programa en ECMAScript ECMAScript programa ECMAScript-ohjelma ECMAScript forrit programme ECMAScript ríomhchlár ECMAScript programa en ECMAScript תכנית EMCAScript ECMAScript program ECMAScript program Programma ECMAScript Program ECMAScript Programma ECMAScript ECMAScript プログラム ECMAScript პროგრამა ECMAScript программасы ECMAScript 프로그램 ECMAScript programa ECMAScript programma ECMAScript-program ECMAScript-programma ECMAScript-program programa ECMAEscript Pogram ECMAScript programa ECMAScript Programa ECMAScript Program ECMAScript программа ECMAScript Program ECMAScript Programska datoteka ECMAScript Program ECMAScript програм ЕЦМАСкрипте ECMAScript-program ECMAScript programı програма мовою ECMAScript Chương trình ECMAScript ECMAScript 程序 ECMAScript 程式 Sega CD disc image Sega Pico ROM Sega Saturn disc image imatge de disc de Sega Saturn obraz disku pro Sega Saturn Sega Saturn-diskaftryk Sega-Saturn-Datenträgerabbild Εικόνα δίσκου Sega Saturn Sega Saturn disc image imagen de disco de Sega Saturn Sega Saturn disko irudia Sega Saturn -levykuva Sega Saturn slika diska Sega Saturn lemezkép Imagine de disco Sega Saturn Image cakram Sega Saturn Immagine disco Sega Saturn Sega Saturn диск бейнесі 세가 새턴 디스크 이미지 imatge disc Sega Saturn Obraz płyty konsoli Sega Saturn imagem de disco Sega Saturn Imagem de disco do Sega Saturn образ диска Sega Saturn Obraz disku Sega Saturn слика диска Сега Сатурна Sega Saturn-skivavbild Sega Saturn disk kalıbı образ диска Sega Saturn Sega Saturn 光盘镜像 Sega Saturn 光碟映像檔 Dreamcast GD-ROM GD-ROM de Dreamcast GD-ROM pro Dreamcast Dreamcast GD-ROM Dreamcast GD-ROM Dreamcast GD-ROM Dreamcast GD-ROM GD-ROM de Dreamcast Dreamcast GD-ROM Dreamcast GD-ROM Dreamcast GD-ROM GD-ROM Dreamcast GD-ROM Dreamcast GD-ROM Dreamcast Dreamcast GD-ROM 드림캐스트 GD-ROM GD-ROM Dreamcast Plik GD-ROM konsoli Dreamcast GD-ROM Dreamcast GD-ROM do Dreamcast GD-ROM Dreamcast Dreamcast GD-ROM Дримкаст ГД-РОМ Dreamcast-gd-rom Dreamcast GD-ROM GD-ROM Dreamcast Dreamcast CD-ROM Dreamcast GD-ROM Nintendo DS ROM Nintendo DS ROM Nintendo DS ROM ROM — Nintendo DS ROM de Nintendo DS ROM pro Nintendo DS Nintendo DS-rom Nintendo-DS-ROM Nintendo DS ROM Nintendo DS ROM ROM de Nintendo DS Nintendo DS-ko ROMa Nintendo DS-ROM Nintendo DS ROM ROM Nintendo DS ROM Nintendo DS ROM de Nintendo DS ROM של Nintendo Nintendo DS ROM Nintendo DS ROM ROM pro Nintendo DS Memori baca-saja Nintendo DS ROM Nintendo DS Nintendo DS ROM Nintendo DS ROM 닌텐도 DS 롬 Nintendo DS ROM Nintendo DS ROM Nintendo DS-ROM Nintendo-DS-ROM Nintendo DS-ROM ROM Nintendo DS Plik ROM konsoli Nintendo DS ROM Nintendo DS ROM do Nintendo DS ROM Nintendo DS Nintendo DS ROM ROM pre Nintendo DS Bralni pomnilnik Nintendo DS ROM Nintendo DS Нинтендо ДС РОМ Nintendo DS-rom Nintendo DS ROM ППП Nintendo ROM DS Nintendo Nintendo DS ROM 任天堂 DS ROM PC Engine ROM ROM de PC Engine ROM pro PC Engine PC Engine ROM PC-Engine-ROM PC Engine ROM PC Engine ROM ROM de PC Engine PC Engine ROM PC Engine ROM ROM PC Engine ROM de máquina de PC ROM של PC Engine PC Engine ROM PC Engine ROM ROM PC Engine ROM PC Engine ROM PC Engine PC Engine ROM PC 엔진 롬 ROM PC Engine Plik ROM konsoli PC Engine ROM PC Engine ROM de PC Engine PC Engine ROM PC Engine ROM Pomnilnik PC Engine ROM ПЦ Енџин РОМ PC Engine-rom PC Engine ROM ROM для рушія на ПК PC Engine ROM PC Engine ROM Wii disc image imatge de disc de Wii obraz disku pro Wii Wii-diskaftryk Wii-Datenträgerabbild Εικόνα δίσκου Wii Wii disc image imagen de disco de Wii Wii disko irudia Wii-levykuva image disque Wii Imaxe de disco de Wii דמות כונן Wii Wii slika diska Wii lemezkép Imagine de disco Wii Image disk Wii Immagine disco Wii Wii диск бейнесі Wii 디스크 이미지 imatge disc Wii Obraz płyty konsoli Wii imagem de disco Wii Imagem de disco Wii образ диска Wii Obraz disku Wii Odtis diska Wii слика диска Вии-ја Wii-skivavbild Wii disk görüntüsü образ диска Wii Wii光盘镜像 Wii 光碟映像檔 WiiWare bundle paquet de WiiWare balíček pro WiiWare WiiWare-samling WiiWare-Paket WiiWare bundle conjunto de WiiWare WiiWare paket WiiWare csomag Pacchetto WiiWare Bundel WiiWare Bundle WiiWare 위-웨어 번들 lòt WiiWare Pakiet WiiWare pacote WiiWare Pacote WiiWare пакет WiiWare Balík WiiWare ВииВер комплет WiiWare-paket WiiWare paketi пакет WiiWare WiiWare bundle WiiWare 綁包 GameCube disc image imatge de disc de GameCube obraz disku pro GameCube GameCube-diskaftryk GameCube-Datenträgerabbild Εικόνα δίσκου GameCube GameCube disc image imagen de disco de GameCube GameCube disko irudia GameCube-levykuva image disque GameCube Imae de disco de GameCube דמות כונן GameCube GameCube slika diska GameCube lemezkép Imagine de disco GameCube Image disk GameCube Immagine disco GameCube GameCube диск бейнесі 게임큐브 디스크 이미지 imatge disc GameCube Obraz płyty konsoli GameCube imagem de disco GameCube Imagem de disco GameCube образ диска GameCube Obraz disku GameCube Odtis diska GameCube слика диска Гејм Коцке GameCube-skivavbild GameCube disk görüntüsü образ диска GameCube GameCube光盘镜像 GameCube 光碟映像檔 Debian package حزمة ديبيان Debian paketi Pakunak Debian Пакет — Debian paquet Debian balíček Debianu Pecyn Debian Debianpakke Debian-Paket Πακέτο Debian Debian package Debian-pakaĵo paquete de Debian Debian paketea Debian-paketti Debian pakki paquet Debian pacáiste Debian paquete de Debian חבילת דביאן Debian paket Debian-csomag Pacchetto Debian Paket Debian Pacchetto Debian Debian パッケージ Debian-ის პაკეტი Debian дестесі 데비안 패키지 Debian paketas Debian pakotne Pakej Debian Debian pakke Debian-pakket Debian pakke paquet Debian Pakiet Debiana pacote Debian Pacote Debian Pachet Debian пакет Debian Balíček Debianu Datoteka paketa Debian Paketë Debian Дебијанов пакет Debianpaket Debian paketi пакунок Debian Gói Debian Debian 软件包 Debian 軟體包 Qt Designer file ملف Qt Designer Fajł Qt Designer Файл — Qt Designer fitxer de Qt Designer soubor Qt Designer Qt Designer-fil Qt-Designer-Datei Αρχείο Qt Designer Qt Designer file dosiero de Qt Designer archivo de Qt Designer Qt Designer Fitxategia Qt Designer -tiedosto Qt Designer fíla fichier Qt Designer comhad Qt Designer ficheiro de Qt Designer קובץ של Qt Designer Qt Designer datoteka Qt Designer-fájl File Qt Designer Berkas Qt Designer File Qt Designer Qt Designer ファイル Qt Designer файлы Qt 디자이너 파일 Qt Designer failas Qt Designer datne Fail Qt Designer Qt Designer-fil Qt Designer-bestand Qt Designer-fil fichièr Qt Designer Plik Qt Designer ficheiro do Qt Designer Arquivo do Qt Designer Fișier Qt Designer файл Qt Designer Súbor Qt Designer Datoteka Qt Designer File Qt Designer датотека Кут Дизајнера Qt Designer-fil Qt Tasarımcı dosyası файл програми Qt-дизайнер Tập tin thiết kế Qt Designer Qt Designer 文件 Qt Designer 檔案 Qt Markup Language file Файл — Qt Markup fitxer de llenguatge de marcadors Qt soubor Qt Markup Language Qt Markup Language-fil Qt-Auszeichnungssprachendatei Αρχείο Qt Markup Language Qt Markup Language file archivo de lenguaje de marcado Qt Qt Markup lengoai fitxategia QML-tiedosto fichier Qt Markup Language ficheiro de linguaxe de marcado Qt קובץ שפת סימון של Qt Qt Markup Language datoteka Qt jelölőnyelvű fájl File de linguage de marcation Qt Berkas Bahasa Markup Qt File Qt Markup Language Qt マークアップ言語ファイル Qt Markup Language файлы Qt 마크업 언어 파일 Qt marķēšanas valodas datne Qt Markup Tallbestand fichièr Qt Markup Language Plik języka znaczników Qt ficheiro de linguagem Qt Markup Arquivo de Qt Markup Language файл Qt Markup Language Súbor značkovacieho jazyka Qt Datoteka označevalnega jezika Qt датотека Кутовог језика означавања Qt-märkspråksfil Qt İşaretleme Dili dosyası файл мови розмітки Qt Qt Qt 標記語言檔 desktop configuration file ملف تضبيط سطح المكتب kanfihuracyjny fajł asiarodździa Файл с информация за работния плот fitxer de configuració d'escriptori soubor nastavení pracovní plochy skrivebordskonfigurationsfil Desktop-Konfigurationsdatei Αρχείο ρυθμίσεων επιφάνειας εργασίας desktop configuration file dosiero de agordoj de labortablo archivo de configuración del escritorio Mahaigainaren konfigurazio-fitxategia työpöydän asetustiedosto skriviborðssamansetingarfíla fichier de configuration desktop comhad chumraíocht deisce ficheiro de configuración de escritorio קובץ הגדרות של שולחן העבודה datoteka postavki radne površine asztalbeállító fájl File de configuration de scriptorio berkas konfigurasi destop File configurazione desktop デスクトップ設定ファイル жұмыс үстел баптаулар файлы 데스크톱 설정 파일 darbastalio konfigūracijos failas darbvirsmas konfigurācijas datne Fail konfigurasi desktop konfigurasjonsfil for skrivebordet bureaublad-configuratiebestand skrivebordsoppsettfil fichièr de configuracion desktop Plik konfiguracji środowiska ficheiro de configuração de área de trabalho Arquivo de configuração desktop fișier de configurare al desktopului файл настроек рабочего стола Súbor nastavení pracovnej plochy nastavitvena datoteka namizja File konfigurimi desktop датотека подешавања радне површи skrivbordskonfigurationsfil masa üstü yapılandırma dosyası файл конфігурації стільниці tập tin cấu hình môi trường 桌面配置文件 桌面組態檔 FictionBook document مستند FictionBook Документ — FictionBook document FictionBook dokument FictionBook FictionBook-dokument FictionBook-Dokument Έγγραφο FictionBook FictionBook document FictionBook-dokumento documento FictionBook FictionBook dokumentua FictionBook-asiakirja FictionBook skjal document FictionBook cáipéis FictionBook documento de FictionBook מסמך FictionBook FictionBook dokument FictionBook-dokumentum Documento FictionBook Dokumen FictionBook Documento FictionBook FictionBook ドキュメント FictionBook-ის დოკუმენტი FictionBook құжаты FictionBook 문서 FictionBook dokumentas FictionBook dokuments FictionBook-document document FictionBook Dokument FictionBook documento FictionBook Documento FictionBook Document FictionBook документ FictionBook Dokument FictionBook Dokument FictionBook документ Фикшон Књиге FictionBook-dokument FictionBook belgesi документ FictionBook Tài liệu FictionBook FictionBook 文档 FictionBook 文件 Compressed FictionBook document document FictionBook amb compressió komprimovaný dokument FictionBook Komprimeret FictionBook-dokument Komprimiertes FictionBook-Dokument Συμπιεσμένο έγγραφο FictionBook Compressed FictionBook document documento comprimido de FictionBook Konprimitutako FictionBook dokumentua Pakattu FictionBook-asiakirja document FictionBook compressé Documento de FictionBook comprimida מסמך FictionBook מכווץ Sažet FictionBook dokument Tömörített FictionBook dokumentum Documento FictionBook comprimite Dokumen FictionBook terkompresi Documento FictionBook compresso Сығылған FictionBook құжаты 압축한 FictionBook 문서 document FictionBook compressat Skompresowany dokument FictionBook documento comprimido FictionBook Documento FictionBook comprimido Сжатый документ FictionBook Komprimovaný dokument FictionBook Stisnjeni dokument FictionBook запаковани документ Фикшон Књиге Komprimerat FictionBook-dokument Sıkıştırılmış KurguKitap belgesi стиснений документ FictionBook 压缩的 FictionBook 文档 壓縮的 FictionBook 文件 Dia diagram خطاطة Dia Dia diaqramı Dyjahrama Dia Диаграма — Dia diagrama de Dia diagram Dia Diagram Dia Dia-diagram Dia-Diagramm Διάγραμμα Dia Dia diagram Dia-diagramo diagrama de Dia Dia diagrama Dia-kaavio Dia ritmynd diagramme Dia léaráid Dia diagrama de Dia גרף של Dia Dia dijagram Dia-diagram Diagramma Dia Diagram Dia Diagramma Dia Dia ダイアグラム Dia-ის დიაგრამა Dia диаграммасы Dia 다이어그램 Dia diagrama Dia diagramma Diagram Dia Dia-diagram Dia-diagram Dia diagram diagrama Dia Diagram Dia diagrama Dia Diagrama do Dia Diagramă Dia диаграмма Dia Diagram Dia Datoteka diagrama Dia Diagramë Dia дијаграм Дие Dia-diagram Dia çizimi діаграма Dia Biểu đồ Dia Dia 图表 Dia 圖表 Dia shape شكل Dia Фигура — Dia forma de Dia symboly Dia Dia-figur Dia-Form Σχήμα Dia Dia shape forma de Dia Dia-ren forma Dia-muoto Dia skapur forme Dia cruth Dia forma de Dia צורה של Dia Dia oblik Dia alakzat Forma Dia Shape Dia Sagoma Dia Dia 図形 Dia сызбасы Dia 그림 Dia forma Dia forma Diavorm forma Dia Kształt Dia forma Dia Formato Dia Figură Dia фигура Dia Tvar Dia Datoteka oblik Dia облик Дие Dia-figur Dia şekli форма Dia Dia 形状 Dia 形狀 TeX DVI document مستند TeX DVI Dakument TeX DVI Документ — TeX DVI document DVI de TeX dokument TeX DVI TeX DVI-dokument TeX-DVI-Dokument Έγγραφο TeX DVI TeX DVI document DVI-dokumento de TeX documento TeX DVI TeX DVI dokumentua TeX DVI -asiakirja TeX DVI skjal document TeX DVI cáipéis DVI TeX documento TeX DVI מסמך מסוג TeX DVI TeX DVI dokument TeX DVI-dokumentum Documento TeX DVI Dokumen TeX DVI Documento TeX DVI TeX DVI ドキュメント TeX DVI құжаты TeX DVI 문서 TeX DVI dokumentas TeX DVI dokuments Dokumen TeX DVI TeX DVI-dokument TeX DVI-document TeX DVI-dokument document TeX DVI Dokument TeX DVI documento TeX DVI Documento DVI TeX Document Tex DVI документ TeX DVI Dokument TeX DVI Dokument TeX DVI Dokument TeX DVI ТеКс ДВИ документ TeX DVI-dokument TeX DVI belgesi документ TeX DVI Tài liệu DVI Tex TeX DVI 文档 TeX DVI 文件 DVI Device independent file format Enlightenment theme سمة Enlightenment Enlightenment örtüyü Matyŭ Enlightenment Тема — Enlightenment tema d'Enlightenment motiv Enlightenment Thema Enlightenment Enlightenmenttema Enlightenment-Thema Θέμα Enlightenment Enlightenment theme etoso de Enlightenment tema de Enlightenment Enlightenment gaia Enlightenment-teema Enlightenment tema thème Enlightenment téama Enlightenment tema de Enlightenment ערכת נושא של Enlightenment Enlightenment tema Enlightenment-téma Thema de Enlightenment Tema Enlightenment Tema Enlightenment Enlightenment テーマ Enlightenment-ის თემა Enlightenment темасы 인라이튼먼트 테마 Enlightenment tema Enlightenment motīvs Tema Enlightenment Enlightenment tema Enlightenment-thema Enlightenment-tema tèma Enlightenment Motyw Enlightenment tema Enlightenment Tema do Enlightenment Temă Enlightenment тема Enlightenment Motív Enlightenment Datoteka teme Enlightenment Tema Enlightenment тема Просвећености Enlightenment-tema Enlightenment teması тема Enlightenment Sắc thái Enlightenment Enlightenment 主题 Enlightenment 佈景主題 Egon Animator animation تحريكة محرك Egon Animacyja Egon Animator Анимация — Egon Animator animació d'Egon Animator animace Egon Animator Egon Animator-animation Egon-Animator-Animation Κινούμενο σχέδιο Egon Animator Egon Animator animation animacio de Egon Animator animación de Egon Animator Egon Animator-eko animazioa Egon Animator -animaatio Egon Animator teknimyndagerð animation Egon Animator beochan Egon Animator animación de Egon Animator אנימצייה של Egon Animator Egon Animator animacija Egon Animator-animáció Imagine Egon Animator Animasi Egon Animator Animazione Egon Animator Egon Animator アニメーション Egon Animator-ის ანიმაცია Egon Animator анимациясы Egon 애니메이터 애니메이션 Egon Animator animacija Egon Animator animācija Animasi Egon Animator Egon animator-animasjon Egon Animator-animatie Egon Animator-animasjon animacion Egon Animator Animacja Egon Animator animação Egon Animator Animação do Egon Animator Animație Egon Animator анимация Egon Animator Animácia Egon Animator Datoteka animacije Egon Animator Animim Egon Animator анимација Егон аниматора Egon Animator-animering Egon Animator canlandırması анімація Egon Animator Hoạt ảnh Egon Animator Egon Animator 动画 Egon Animator 動畫 executable تنفيذي vykonvalny fajł Изпълним файл executable spustitelný soubor kørbar Programm Εκτελέσιμο executable plenumebla ejecutable exekutagarria suoritettava ohjelma inningarfør exécutable comhad inrite executábel קובץ הרצה izvršna datoteka futtatható Executabile dapat dieksekusi Eseguibile 実行ファイル орындалатын 실행 파일 vykdomasis failas izpildāmais Bolehlaksana kjørbar uitvoerbaar bestand køyrbar executable Program executável Executável executabil исполняемый Spustiteľný súbor izvedljiva datoteka I ekzekutueshëm извршна körbar fil çalıştırılabilir виконуваний файл thực hiện được 可执行文件 可執行檔 FLTK Fluid file ملف FLTK Fluid Fajł FLTK Fluid Интерфейс — FLTK Fluid fitxer FLTK Fluid soubor FLTK Fluid FLTK Fluid-fil FLTK-Fluid-Datei Αρχείο FLTK Fluid FLTK Fluid file archivo FLTK Fluid FLTK Fluid fitxategia FLTK Fluid -tiedosto FLTK Fluid fíla fichier Fluid FLTK comhad FLTK Fluid ficheiro FLTK Fluid קובץ FLTK Fluid FLTK Fluid datoteka FLTK Fluid fájl File Fluid de FLTK Berkas FLTK Fluid File FLTK Fluid FLTK Fluid ファイル FLTK Fluid-ის ფაილი FLTK Fluid файлы FLTK Fluid 파일 FLTK Fluid failas FLTK Fluid datne FLTK Fluid-fil FLTK FLUID-bestand FLTK Fluid-fil fichièr Fluid FLTK Plik Fluid FLTK ficheiro FLTK Fluid Arquivo Fluid do FLTK Fișier FLTK Fluid файл FLTK Fluid Súbor FLTK Fluid Datoteka FLTK Fluid File FLTK Fluid датотека ФЛТК Флуида FLTK Fluid-fil FLTK Fluid dosyası файл FLTK Fluid Tập tin Fluid FLTK FLTK 流体文档 FLTK Fluid 檔 FLTK Fast Light Toolkit WOFF font tipus de lletra WOFF písmo WOFF WOFF-skrifttype WOFF-Schrift Γραμματοσειρά WOFF WOFF font tipo de letra WOFF WOFF letra-tipoa WOFF-fontti police WOFF Tipo de letra WOFF גופן WOFF WOFF slovo WOFF-betűkészlet Typo de litteras WOFF Fonta WOFF Font WOFF WOFF フォント WOFF қарібі WOFF 글꼴 WOFF fonts poliça WOFF Czcionka WOFF letra WOFF Fonte WOFF шрифт WOFF Písmo WOFF Pisava WOFF ВОФФ слова WOFF-typsnitt WOFF yazı tipi шрифт WOFF WOFF 字体 WOFF 字型 WOFF Web Open Font Format Postscript type-1 font خط Postscript type-1 Šryft Postscript type-1 Шрифт — Postscript Type 1 tipus de lletra Postscript type-1 písmo Postscript type-1 PostScript type-1-skrifttype Postscript-Typ-1-Schrift Γραμματοσειρά Postscript type-1 Postscript type-1 font tipo de letra PostScript Type-1 PostScript type-1 letra-tipoa PostScript tyyppi-1 -fontti Postscript type-1 stavasnið police Postscript Type 1 cló Postscript type-1 tipo de letra PostScript tipo-1 גופן של Postscript type-1 Postscript crsta-1 slovo Postscript type-1 betűkészlet Typo de litteras PostScript typo 1 Fonta tipe-1 Postscript Carattere Postscript type-1 PostScript type-1 フォント Postscript type-1 қарібі PostScript Type-1 글꼴 Postscript type-1 šriftas Postscript 1-tipa fonts Postscript type-1 skrift PostScript type-1-lettertype PostScript type 1-skrifttype poliça Postescript Type 1 Czcionka PostScript Type-1 letra PostScript tipo 1 Fonte PostScript tipo-1 Font Postscript type-1 шрифт PostScript Type-1 Písmo Postscript type-1 Datoteka pisave Postscript vrste-1 Lloj gërmash Postscript type-1 слова Постскрипта врсте-1 Postscript type-1-typsnitt Postscript type-1 yazı tipi шрифт Postscript type-1 Phông kiểu 1 PostScript Postscript type-1 字体 Postscript type-1 字型 Adobe font metrics مقاييس خط أدوبي Adobe yazı növü metrikləri Metryka šryftu Adobe Шрифтова метрика — Adobe mètrica de tipus de lletra Adobe metrika písma Adobe Metrigau Ffont Adobe Adobe skrifttypefil Adobe-Schriftmetriken Μετρικά γραμματοσειράς Adobe Adobe font metrics metrikoj de Adobe-tiparo métricas tipográficas de Adobe Adobe letra-tipoen neurriak Adobe-fonttimitat métriques de police Adobe meadarachtaí cló Adobe métricas de fonte de Adobe מדדי גופן של Adobe Adobe mjere fonta Adobe-betűmetrika Metricas de typo de litteras Adobe Metrik fonta Adobe Metriche tipo carattere Adobe Adobe フォントメトリック Adobe қаріп метрикалары Adobe 글꼴 메트릭 Adobe šriftų metrika Adobe fonta metrika Metrik font Adobe Adobe skrifttypefil Adobe-lettertype-metrieken Adobe skrifttypemetrikk metricas de poliça Adobe Metryka czcionki Adobe métrica de letras Adobe Métricas de fonte Adobe Dimensiuni font Adobe метрика шрифта Adobe Metrika písma Adobe Matrika pisave Adobe Metrik lloj gërmash Adobe метрика Адобе слова Adobe-typsnittsmetrik Adobe yazıtipi ölçüleri метрики шрифту Adobe Cách đo phông chữ Adobe Adobe 字体参数 Adobe 字型描述檔 BDF font خط BDF BDF yazı növü Šryft BDF Шрифт — BDF tipus de lletra BDF písmo BDF Ffont BDF BDF-skrifttype BDF-Schrift Γραμματοσειρά BDF BDF font BDF-tiparo tipo de letra BDF BDF letra-tipoa BDF-fontti BDF stavasnið police BDF cló BDF tipo de fonte BDF גופן BDF BDF font BDF-betűkészlet Typo de litteras BDF Fonta BDF Carattere BDF BDF フォント BDF қарібі BDF 글꼴 BDF šriftas BDF fonts Font BDF BDF-skrifttype BDF-lettertype BDF-skrifttype poliça BDF Czcionka BDF letra BDF Fonte BDF Font BDF шрифт BDF Písmo BDF Datoteka pisave BDF Lloj gërme BDF БДФ слова BDF-typsnitt BDF fontu шрифт BDF Phông chữ BDF BDF 字体 BDF 字型 DOS font خط DOS DOS yazı növü Šryft DOS Шрифт — DOS tipus de lletra DOS písmo pro DOS Ffont DOS DOS-skrifttype DOS-Schrift Γραμματοσειρά DOS DOS font DOS-tiparo tipo de letra de DOS DOS letra-tipoa DOS-fontti DOS stavasnið police DOS cló DOS tipo de fonte de DOS גופן DOS DOS font DOS-betűkészlet Typo de litteras DOS Fonta DOS Carattere DOS DOS フォント DOS қарібі DOS 글꼴 DOS šriftas DOS fonts Font DOS DOS-skrifttype DOS-lettertype DOS-skrifttype poliça DOS Czcionka DOS letra DOS Fonte do DOS Font DOS шрифт DOS Písmo pre DOS Datoteka pisave DOS Gërmë DOS ДОС слова DOS-typsnitt DOS fontu шрифт DOS Phông chữ DOS DOS 字体 DOS 字型 Adobe FrameMaker font خط أدوبي الصانع للإطارات Adobe FrameMaker yazı növü Šryft Adobe FrameMaker Шрифт — Adobe FrameMaker tipus de lletra d'Adobe FrameMaker písmo Adobe FrameMaker Ffont Adobe FrameMaker Adobe FrameMaker-skrifttype Adobe-FrameMaker-Schrift Γραμματοσειρά Adobe FrameMaker Adobe FrameMaker font Tiparo de Adobe FrameMaker tipo de letra de Adobe FrameMaker Adobe FrameMaker-en letra-tipoa Adobe FrameMaker -fontti Adobe FrameMaker stavasnið police Adobe FrameMaker cló Adobe FrameMaker tipo de fonte de Adobe FrameMaker גופן של Adobe FrameMaker Adobe FrameMaker font Adobe FrameMaker-betűkészlet Typo de litteras pro Adobe FrameMaker Fonta Adobe FrameMaker Carattere Adobe FrameMaker Adobe FrameMaker フォント Adobe FrameMaker қарібі Adobe 프레임메이커 글꼴 Adobe FrameMaker šriftas Adobe FrameMaker fonts Font Adobe FrameMaker Adobe FrameMaker skrifttype Adobe FrameMaker-lettertype Adobe FrameMaker-skrifttype poliça Adobe FrameMaker Czcionka Adobe FrameMaker letra Adobe FrameMaker Fonte do Adobe FrameMaker Font Adobe FrameMaker шрифт Adobe FrameMaker Písmo Adobe FrameMaker Datoteka pisave Adobe FrameMaker Gërma Adobe FrameMaker слова Адобе Фрејм Мејкера Adobe FrameMaker-typsnitt Adobe FrameMaker yazı tipi шрифт Adobe FrameMaker Phông chữ Adobe FrameMaker Adobe FrameMaker 字体 Adobe FrameMaker 字型 LIBGRX font خط LIBGRX LIBGRX yazı növü Šryft LIBGRX Шрифт — LIBGRX tipus de lletra LIBGRX písmo LIBGRX Ffont LIBGRX LIBGRX-skrifttype LIBGRX-Schrift Γραμματοσειρά LIBGRX LIBGRX font LIBGRX-tiparo tipo de letra LIBGRX LIBGRX letra-tipoa LIBGRX-fontti LIBGRX stavasnið police LIBGRX cló LIBGRX tipo de fonte en LIBGRX גופן LIBGRX LIBGRX font LIBGRX-betűkészlet Typo de litteras LIBGRX Fonta LIBGRX Carattere LIBGRX LIBGRX フォーマット LIBGRX қарібі LIBGRX 글꼴 LIBGRX šriftas LIBGRX fonts Font LIBGRX LIBGRX-skrifttype LIBGRX-lettertype LIBGRX skrifttype poliça LIBGRX Czcionka LIBGRX letra LIBGRX Fonte LIBGRX Font LIBGRX шрифт LIBGRX Písmo LIBGRX Datoteka pisave LIBGRX Lloj gërme LIBGRX ЛИБГРИкс слова LIBGRX-typsnitt LIBGRX fontu шрифт LIBGRX Phông chữ LIBGRX LIBGRX 字体 LIBGRX 字型 Linux PSF console font خط كونسول PSF لينكس Linux PSF konsol yazı növü Kansolny šryft PSF dla Linuksa Шрифт — PSF, за конзолата на Линукс tipus de lletra de consola Linux PSF písmo PSF pro konzolu Linuxu Ffont Linux PSF Linux PSF-konsolskrifttype Linux-PSF-Konsolenschrift Γραμματοσειρά κονσόλας PSF Linux Linux PSF console font PSF-tiparo de Linux-konzolo tipo de letra de consola Linux PSF Linux PSF kontsolako letra-tipoa Linux PSF -konsolifontti Linux PSF stýristøðs stavasnið police console Linux PSF cló chonsól Linux PSF tipo de fonte de consola Linux PSF גופן לקונסול מסוג Linux PSF Linux PSF konzolni font Linux PSF konzolos betűkészlet Typo de litteras console Linux PSF Fonta konsol Linux PSF Carattere console Linux PSF Linux PSF コンソールフォント Linux PSF консольдік қарібі 리눅스 PSF 콘솔 글꼴 Linux PSF konsolės šriftas Linux PSF konsoles fonts Font konsol PSF Linux Linux PSF konsollskrifttype Linux PSF-console-lettertype Linux PSF konsoll-skrifttype poliça consòla Linux PSF Czcionka konsoli PSF Linux letra de consola Linux PSF Fonte de console Linux PSF Font consolă Linux PSF консольный шрифт Linux PSF Písmo PSF pre konzolu Linuxu Datoteka pisave konzole Linux PSF Lloj gërme për konsolë Linux PSF слова Линуксове ПСФ конзоле Linux PSF-konsoltypsnitt Linux PSF konsol fontu консольний шрифт Linux PSF Phông chữ bàn giao tiếp PSF Linux Linux PSF 控制台字体 Linux PSF console 字型 Linux PSF console font (gzip-compressed) خط كونسول PSF لينكس (مضغوط-gzip) Kansolny šryft PSF dla Linuksa (gzip-skampresavany) Шрифт — Linux PSF, компресиран с gzip tipus de lletra de consola Linux PSF (amb compressió gzip) písmo PSF pro konzolu Linuxu (komprimované pomocí gzip) Linux PSF-konsolskrifttype (gzip-komprimeret) Linux-PSF-Konsolenschrift (gzip-komprimiert) Γραμματοσειρά κονσόλας PSF Linux (συμπιεσμένη με gzip) Linux PSF console font (gzip-compressed) tipo de letra de consola Linux PSF (comprimido con gzip) Linux PSF kontsolako letra-tipoa (gzip-ekin konprimitua) Linux PSF -konsolifontti (gzip-pakattu) Linux PSF stýristøðs stavasnið (gzip-stappað) police console Linux PSF (compressée gzip) cló chonsól Linux PSF (comhbhrúite le gzip) tipo de fonte de consola Linux PSF (comprimida con gzip) גופן למסוף מסוג Linux PSF (מכווץ ע״י gzip) Linux PSF konzolni font (komprimiran gzip-om) Linux PSF konzolos betűkészlet (gzip-tömörítésű) Typo de litteras console Linux PSF (comprimite con gzip) Fonta konsol Linux PSF (terkompresi gzip) Carattere console Linux PSF (compresso con gzip) Linux PSF コンソールフォント (gzip 圧縮) Linux PSF консольдік қарібі (gzip-пен сығылған) 리눅스 PSF 콘솔 글꼴(GZIP 압축) Linux PSF konsolės šriftas (suglaudintas su gzip) Linux PSF konsoles fonts (saspiests ar gzip) Linux PSF konsollskrifttype (gzip-komprimert) Linux PSF-console-lettertype (ingepakt met gzip) Linux PSF konsoll-skrifttype (gzip-pakka) poliça consòla Linux PSF (compressat gzip) Czcionka konsoli PSF Linux (kompresja gzip) letra de consola Linux PSF (compressão gzip) Fonte de console Linux PSF (compactada com gzip) Font consolă Linux PSF (compresie gzip) консольный шрифт Linux PSF (сжатый gzip) Písmo PSF pre konzolu Linuxu (komprimované pomocou gzip) Datoteka pisave konzole Linux PSF (skrčena z gzip) Lloj gërme për konsolë Linux PSF (komresuar me gzip) слова Линуксове ПСФ конзоле (запакована гзип-ом) Linux PSF-konsoltypsnitt (gzip-komprimerat) Linux PSF konsol fontu (gzip ile sıkıştırılmış) консольний шрифт Linux PSF (стиснений gzip) Phông chữ bàn giao tiếp PSF Linux (đã nén gzip) Linux PSF 控制台字体(gzip 压缩) Linux PSF console 字型 (gzip 格式壓縮) PCF font خط PCF PCF yazı növü Šryft PCF Шрифт — PCF tipus de lletra PCF písmo PCF Ffont PCF PCF-skrifttype PCF-Schrift Γραμματοσειρά PCF PCF font PCF-tiparo tipo de letra PCF PCF letra-tipoa PCF-fontti PCF stavasnið police PCF cló PCF tipo de letra PCF גופן PCF PCF slovo PCF-betűkészlet Typo de litteras PCF Fonta PCF Carattere PCF PCF フォント PCF қарібі PCF 글꼴 PCF šriftas PCF fonts Font PCF PCF-skrifttype PCF-lettertype PCF-skrifttype poliça PCF Czcionka PCF letra PCF Fonte PCF Font PCF шрифт PCF Písmo PCF Datoteka pisave PCF Gërma PCF ПЦФ слова PCF-typsnitt PCF fontu шрифт PCF Phông chữ PCF PCF 字体 PCF 字型 OpenType font خط OpenType OpenType yazı növü Šryft OpenType Шрифт — OpenType tipus de lletra OpenType písmo OpenType Ffont OpenType OpenType-skrifttype OpenType-Schrift Γραμματοσειρά OpenType OpenType font OpenType-tiparo tipo de letra OpenType OpenType letra-tipoa OpenType-fontti OpenType stavasnið police OpenType cló OpenType tipo de fonte OpenType גופן של OpenType OpenType slovo OpenType-betűkészlet Typo de litteras OpenType Fonta OpenType Carattere OpenType OpenType フォント OpenType қарібі 오픈타입 글꼴 OpenType šriftas OpenType fonts Font OpenType OpenType-skrifttype OpenType-lettertype OpenType-skrifttype poliça OpenType Czcionka OpenType letra OpenType Fonte OpenType Font OpenType шрифт OpenType Písmo OpenType Datoteka pisave OpenType Gërma OpenType слова Отворене Врсте OpenType-typsnitt OpenType fontu шрифт OpenType Phông chữ OpenType OpenType 字体 OpenType 字型 Speedo font خط Speedo Speedo yazı növü Šryft Speedo Шрифт — Speedo tipus de lletra Speedo písmo Speedo Ffont Speedo Speedoskrifttype Speedo-Schrift Γραμματοσειρά Speedo Speedo font Speedo-tiparo tipo de letra de Speedo Speedo letra-tipoa Speedo-fontti Speedo stavasnið police Speedo cló Speedo tipo de letra Speedo גופן של Speedo Speedo font Speedo-betűkészlet Typo de litteras Speedo Fonta Speedo Carattere Speedo Speedo フォント Speedo қарібі Speedo 글꼴 Speedo šriftas Speedo fonts Font Speedo Speedo-skrifttype Speedo-lettertype Speedo-skrifttype poliça Speedo Czcionka Speedo letra Speedo Fonte Speedo Font Speedo шрифт Speedo Písmo Speedo Datoteka pisave Speedo Gërma Speedo Спидо слова Speedo-typsnitt Speedo fontu шрифт Speedo Phông chữ Speedo Speedo 字体 Speedo 字型 SunOS News font خط SunOS News SunOS News yazı növü Šryft SunOS News Шрифт — SunOS News tipus de lletra SunOS News písmo SunOS News Ffont SunOS News SunOS News-skrifttype SunOS-News-Schrift Γραμματοσειρά SunOS News SunOS News font tiparo de SunOS News tipo de letra para NeWS de SunOS SunOs News letra-tipoa SunOS News -fontti SunOS News stavasnið police SunOS News cló SunOS News tipo de letra SunOS News גופן של SunOS News SunOS News font SunOS News-betűkészlet Typo de litteras SunOS News Fonta SunOS News Carattere SunOS News SunOS News フォント SunOS News қарібі SunOS News 글꼴 SunOS News šriftas SunOS News fonts Font News SunOS SunOS News-skrifttype SunOS News-lettertype SunOS NEWS-skrifttype poliça SunOS News Czcionka SunOS News letra SunOS News Fonte SunOS News Font SunOS News шрифт SunOS News Písmo SunOS News Datoteka pisave SunOS News Gërma SunOS News слова СанОС Њуза SunOS News-typsnitt SunOS News yazı tipi шрифт SunOS News Phông chữ SunOS News SunOS News 字体 SunOS News 字型 TeX font خط TeX TeX yazı növü Šryft TeX Шрифт — TeX tipus de lletra TeX písmo TeX Ffont TeX TeX-skrifttype TeX-Schrift Γραμματοσειρά TeX TeX font TeX-tiparo tipo de letra de TeX TeX letra-tipoa TeX-fontti TeX stavasnið police TeX cló TeX tipo de letra de TeX גופן TeX TeX font TeX-betűkészlet Typo de litteras TeX Fonta TeX Carattere TeX TeX フォント TeX қарібі TeX 글꼴 TeX šriftas TeX fonts Font TeX TeX-skrift TeX-lettertype TeX-skrifttype poliça TeX Czcionka TeX letra TeX Fonte TeX Font TeX шрифт TeX Písmo TeX Datoteka pisave TeX Gërma TeX ТеКс слова TeX-typsnitt TeX fontu шрифт TeX Phông chữ TeX TeX 字体 TeX 字型 TeX font metrics مقاييس خط TeX TeX yazı növü metrikləri Metryka šryftu TeX Шрифтова метрика — TeX mètrica de tipus de lletra TeX metrika písma TeX Metrigau Ffont TeX TeX-skrifttypeinformation TeX-Schriftmetriken Μετρικά γραμματοσειράς TeX TeX font metrics metrikoj de TeX-tiparo métricas tipográficas de TeX TeX letra-tipoen neurriak TeX-fonttimitat métriques de police TeX meadarachtaí cló TeX Métricas de tipo de letra de TeX ממדי גופן של TeX TeX mjere fonta TeX-betűmetrika Metricas de typo de litteras TeX Fonta metrik TeX Metriche tipo carattere TeX TeX フォントメトリック TeX қаріп метрикалары TeX 글꼴 메트릭 TeX šriftų metrika TeX fonta metrikas Metrik font TeX TeX skrifttypemetrikk TeX-lettertype-metrieken TeX skrifttypemetrikk metricas de poliça TeX Metryki czcionki TeX métricas de letra TeX Métrica de fonte TeX Dimensiuni font TeX метрика шрифта TeX Metrika písma TeX Matrika pisave Tex Gërma TeX metrics метрика слова ТеКс-а TeX-typsnittsmetrik TeX yazı tipi ölçüleri метрики шрифту TeX Cách đo phông chữ TeX TeX 字体参数 TeX 字型描述檔 TrueType font خط TrueType Šryft TrueType Шрифт — TrueType tipus de lletra TrueType písmo TrueType TrueType-skrifttype TrueType-Schrift Γραμματοσειρά TrueType TrueType font TrueType-tiparo tipo de letra TrueType TrueType letra-tipoa TrueType-fontti TrueType stavasnið police Truetype cló TrueType tipo de letra TrueType גופן מסוג TrueType TrueType font TrueType-betűkészlet Typo de litteras TrueType Fonta TrueType Carattere TrueType TrueType フォント TrueType қарібі 트루타입 글꼴 TrueType šriftas TrueType fonts Font TrueType TrueType-skrift TrueType-lettertype TrueType-skrifttype poliça Truetype Czcionka TrueType letra TrueType Fonte TrueType Font TrueType шрифт TrueType Písmo TrueType Datoteka pisave TrueType Lloj gërme TrueType Трутајп слова Truetype-typsnitt TrueType fontu шрифт TrueType Phông chữ TrueType TrueType 字体 TrueType 字型 TrueType XML font خط TrueType XML Šryft TrueType XML Шрифт — TrueType XML tipus de lletra TrueType XML písmo TrueType XML TrueType XML-skrifttype TrueType-XML-Schrift Γραμματοσειρά XML TrueType TrueType XML font tipo de letra TrueType XML TrueType XML letra-tipoa TrueType-XML-fontti TrueType XML stavasnið police Truetype XML cló XML TrueType tipo de letra TrueType XML גופן XML מסוג TrueType TrueType XML font TrueType XML betűkészlet Typo de litteras TrueType XML Fonta TrueType XML Carattere TrueType XML TrueType XML フォント TrueType XML қарібі 트루타입 XML 글꼴 TrueType XML šriftas TrueType XML fonts TrueType XML-skrift TrueType XML-lettertype TrueType XML-skrifttype poliça Truetype XML Czcionka TrueType XML letra TrueType XML Fonte TrueType XML Font XML TrueType шрифт TrueType XML Písmo TrueType XML Datoteka pisave TrueType XML Lloj gërme TrueType XML Трутајп ИксМЛ слова Truetype XML-typsnitt TrueType XML fontu шрифт TrueType XML Phông chữ XML TrueType TrueType XML 字体 TrueType XML 字型 V font خط V V yazı növü Šryft V Шрифт — V tipus de lletra V písmo V Ffont V V-skrifttype V-Schrift Γραμματοσειρά V V font V-tiparo tipo de letra V V letra-tipoa V-fontti V stavasnið police V cló V tipo de letra V גופן של V V font V-betűkészlet Typo de litteras V Fonta V Carattere V V フォント V font қарібі V 글꼴 V šriftas V fonts Font V V-skrift V-lettertype V-skrifttype poliça V Czcionka V letra V Fonte V Font V шрифт V font Písmo V Datoteka pisave V Gërmë V В слова V-typsnitt V fontu V-шрифт Phông chữ V V 字体 V 字型 Adobe FrameMaker document مستند أدوبي الصانع للإطارات Dakument Adobe FrameMaker Документ — Adobe FrameMaker document d'Adobe FrameMaker dokument Adobe FrameMaker Adobe FrameMaker-dokument Adobe-FrameMaker-Dokument Έγγραφο Adobe FrameMaker Adobe FrameMaker document Dokumento de Adobe FrameMaker documento de Adobe FrameMaker Adobe FrameMaker-en dokumentua Adobe FrameMaker -asiakirja Adobe FrameMaker skjal document Adobe FrameMaker cáipéis Adobe FrameMaker documento de Adobe FrameMaker מסמך Adobe FrameMaker Adobe FrameMaker dokument Adobe FrameMaker-dokumentum Documento Adobe FrameMaker Dokumen Adobe FrameMaker Documento Adobe FrameMaker Adobe FrameMaker ドキュメント Adobe FrameMaker-ის დოკუმენტი Adobe FrameMaker құжаты Adobe 프레임메이커 문서 Adobe FrameMaker dokumentas Adobe FrameMaker dokuments Adobe FrameMaker-dokument Adobe FrameMaker-document Adobe FrameMaker-dokument document Adobe FrameMaker Dokument Adobe FrameMaker documento Adobe FrameMaker Documento do Adobe FrameMaker Document Adobe FrameMaker документ Adobe FrameMaker Dokument Adobe FrameMaker Dokument Adobe FrameMaker Dokument Adobe FrameMaker документ Адобе Фреј Мејкера Adobe FrameMaker-dokument Adobe FrameMaker belgesi документ Adobe FrameMaker Tài liệu Adobe FrameMaker Adobe FrameMaker 文档 Adobe FrameMaker 文件 Game Boy ROM Game Boy ROM Game Boy ROM ROM — Game Boy ROM de Game Boy ROM pro Game Boy Game Boy-rom Game-Boy-ROM Game Boy ROM Game Boy ROM NLM de Game Boy ROM de Game Boy Game Boy-eko ROMa Game Boy -ROM Game Boy ROM ROM Game Boy ROM Game Boy ROM de Game Boy ROM של Game Boy Game Boy ROM Game Boy ROM ROM de Game Boy Memori baca-saja Game Boy ROM Game Boy ゲームボーイ ROM Game Boy-ის ROM Game Boy ROM 게임보이 롬 Game Boy ROM Game Boy ROM ROM Game Boy Game Boy-ROM Game Boy-ROM Game Boy-ROM ROM Game Boy Plik ROM konsoli Game Boy ROM Game Boy ROM do Game Boy ROM Game Boy Game Boy ROM ROM pre Game Boy Bralni pomnilnik Game Boy ROM Game Boy Гејм Бој РОМ Game Boy-rom Game Boy ROM ППП Game Boy ROM Game Boy Game Boy ROM Game Boy ROM Game Boy Color ROM Game Boy Advance ROM Game Boy Advance ROM Game Boy Advance ROM ROM — Game Boy Advance ROM de Game Boy Advance ROM pro Game Boy Advance Game Boy Advance-rom Game-Boy-Advance-ROM Game Boy Advance ROM Game Boy Advance ROM ROM de Game Boy Advance Game Boy Advance-ko ROMa Game Boy Advance -ROM Game Boy Advance ROM ROM Game Boy Advance ROM Game Boy Advance ROM de Game Boy Advance ROM של Game Boy Advance Game Boy Advance ROM Game Boy Advance ROM ROM de Game Boy Advance Memori baca-saja Game Boy Advance ROM Game Boy Advance ゲームボーイアドバンス ROM Game Boy Advance-ის ROM Game Boy Advance ROM 게임보이 어드밴스 롬 Game Boy Advance ROM Game Boy Advance ROM Game Boy Advance-ROM Game Boy Advance-ROM Game Boy Advance-ROM ROM Game Boy Advance Plik ROM konsoli Game Boy Advance ROM Game Boy Advance ROM do Game Boy Advance ROM Game Boy Advance Game Boy Advance ROM ROM pre Game Boy Advance Bralni pomnilnik Game Boy Advance ROM Game Boy Advance Гејм Бој Адванс РОМ Game Boy Advance-rom Game Boy Gelişmiş ROM розширений ППП Game Boy ROM Game Boy Advance Game Boy Advance ROM Game Boy Advance ROM GDBM database قاعدة بيانات GDBM Baza źviestak GDBM База от данни — GDBM base de dades GDBM databáze GDBM GDBM-database GDBM-Datenbank Βάση δεδομένων GDBM GDBM database GDBM-datumbazo base de datos GDBM GDBM datu-basea GDBM-tietokanta GDBM dátustovnur base de données GDBM bunachar sonraí GDBM base de datos GDBM מסד נתונים GDBM GDBM baza podataka GDBM adatbázis Base de datos GDBM Basis data GDBM Database GDBM GDBM データベース GDBM მონაცემთა ბაზა GDBM дерекқоры GDBM 데이터베이스 GDBM duomenų bazė GDBM datubāze GDBM-database GDBM-gegevensbank GDBM-database banca de donadas GDBM Baza danych GDBM base de dados GDMB Banco de dados GDBM Bază de date GDBM база данных GDBM Databáza GDBM Podatkovna zbirka GDBM Bazë me të dhëna GDBM ГДБМ база података GDBM-databas GDBM veritabanı база даних GDBM Cơ sở dữ liệu GDBM GDBM 数据库 GDBM 資料庫 GDBM GNU Database Manager Genesis ROM Genesis ROM Genesis ROM ROM — Genesis ROM de Genesis ROM pro Genesis Genesis-rom Genesis-ROM Genesis ROM Genesis ROM Genesis-NLM ROM de Genesis (Mega Drive) Genesis-eko ROMa Genesis-ROM Genesis ROM ROM Mega Drive/Genesis ROM Genesis ROM xenérica ROM של Genesis Genesis ROM Genesis ROM ROM de Mega Drive/Genesis Memori baca-saja Genesis ROM Megadrive メガドライブ ROM Genesis ROM 제네시스 롬 Genesis ROM Genesis ROM ROM Genesis Genesis-ROM Mega Drive Genesis-ROM ROM Mega Drive/Genesis Plik ROM konsoli Mega Drive ROM Mega Drive ROM do Gênesis (Mega Drive) ROM Genesis Genesis ROM ROM pre Megadrive Bralni pomnilnik Genesis ROM Genesis Мегадрајв РОМ Mega Drive-rom Genesis ROM ППП Genesis ROM Genesis Genesis ROM Genesis ROM Genesis 32X ROM translated messages (machine-readable) رسائل مترجمة (مقروءة آليا) pierakładzienyja paviedamleńni (dla čytańnia kamputaram) Преведени съобщения — машинен формат missatges traduïts (llegible per màquina) přeložené zprávy (strojově čitelné) oversatte meddelelser (maskinlæsbare) Übersetzte Meldungen (maschinenlesbar) Μεταφρασμένα μηνύματα (για μηχανική ανάγνωση) translated messages (machine-readable) tradukitaj mesaĝoj (maŝinlegebla) mensajes traducidos (legibles por máquinas) itzulitako mezuak (ordenagailuek irakurtzeko) käännetyt viestit (koneluettava) týdd boð (maskin-lesifør) messages traduits (lisibles par machine) teachtaireachtaí aistrithe (inléite ag meaisín) mensaxes traducidos (lexíbeis por máquinas) הודעות מתורגמות (מובן ע״י מכונה) prevedene poruke (strojno čitljive) lefordított üzenetek (gépi kód) messages traducite (legibile pro machinas) pesan diterjemahkan (dapat dibaca mesin) Messaggi tradotti (leggibili da macchina) 翻訳メッセージ (マシン用) ნათარგმნი შეტყობინებები (მანქანისთვის განკუთვნილი) аударылған хабарламалар (машиналық түрде) 번역 메시지(컴퓨터 사용 형식) išversti užrašai (kompiuteriniu formatu) pārtulkotie ziņojumi (mašīnlasāms) Mesej diterjemah (bolehdibaca-mesin) oversatte meldinger (maskinlesbar) vertaalde berichten (machine-leesbaar) oversette meldingar (maskinlesbare) messatges tradusits (legibles per maquina) Przetłumaczone komunikaty (czytelne dla komputera) mensagens traduzidas (leitura pelo computador) Mensagens traduzidas (legível pelo computador) mesaje traduse (citite de calculator) переводы сообщений (откомпилированые) Preložené správy (strojovo čitateľné) prevedena sporočila (strojni zapis) Mesazhe të përkthyer (të lexueshëm nga makina) преведене поруке (машинама читљиве) översatta meddelanden (maskinläsbara) çevrilmiş iletiler (makine tarafından okunabilir) перекладені повідомлення (у машинній формі) thông điệp đã dịch (máy đọc được) 消息翻译(机读) 翻譯訊息 (程式讀取格式) GTK+ Builder constructor de GTK+ GTK+ Builder GTK+ Builder GTK+ Builder Δομητής GTK+ GTK+ Builder GTK+ Builder GTK+ Builder GTK+ Builder GTK+ Builder Construtor de GTK+ בנייה של GTK+‎ GTK+ Builder GTK+ Builder GTK+ Builder GTK+ Builder GTK+ Builder GTK+ Builder GTK+ Builder GTK+ 빌더 GTK+ būvētājs GTK+ Builder GTK+ Builder Construtor GTK+ GTK+ Builder GTK+ Builder GTK+ Builder GTK+ Builder ГТК+ Градитељ GTK+ Builder GTK+ İnşa Edici GTK+ Builder GTK+ 构建程序 GTK+ Builder Glade project مشروع Glade Glade layihəsi Prajekt Glade Проект — Glade projecte de Glade projekt Glade Prosiect Glade Gladeprojekt Glade-Projekt Έργο Glade Glade project Glade-projekto proyecto de Glade Glade proiektua Glade-projekti Glade verkætlan projet Glade tionscadal Glade proxecto de Glade מיזם Glade Glade projekt Glade-projekt Projecto Glade Proyek Glade Progetto Glade Glade プロジェクト Glade жобасы Glade 프로젝트 Glade projektas Glade projekts Projek Glade Glade prosjekt Glade-project Glade prosjekt projècte Glade Projekt Glade projecto Glade Projeto do Glade Proiect Glade проект Glade Projekt Glade Datoteka projekta Glade Projekt Glade Глејдов пројекат Glade-projekt Glade projesi проект Glade Dự án Glade Glade 工程 Glade 專案 GnuCash financial data معلومات GnuCash المالية Финансови данни — GnuCash dades financeres de GnuCash finanční data GnuCash Finansielle data til GnuCash GnuCash-Finanzdaten Οικονομικά στοιχεία GnuCash GnuCash financial data datos financieros de GnuCash GnuCash finantzako datuak GnuCash-taloustiedot GnuCash fíggjarligar dátur données financières GnuCash sonraí airgeadúla GnuCash datos financeiros de GNUCash מידע כלכלי של GnuCash GnuCash financijski podaci GnuCash pénzügyi adatok Datos financiari GnuCash Data keuangan GnuCash Dati finanziari GnuCash GnuCash 会計データ GnuCash қаржы ақпараты GnuCash 재정 자료 GnuCash finansiniai duomenys GnuCash finanšu dati GnuCash financiële gegevens donadas financières GnuCash Dane finansowe GnuCash dados financeiros GnuCash Dados financeiros do GnuCash Date financiare GnuCash финансовые данные GnuCash Finančné údaje GnuCash Datoteka finančnih podatkov GnuCash финансијски подаци Гнуовог новца GnuCash-finansdata GnuCash mali verisi фінансові дані GnuCash Dữ liệu tài chính GnuCash GnuCash 财务数据 GnuCash 財務資料 Gnumeric spreadsheet جدول Gnumeric Raźlikovy arkuš Gnumeric Таблица — Gnumeric full de càlcul de Gnumeric sešit Gnumeric Gnumeric-regneark Gnumeric-Tabelle Λογιστικό φύλλο Gnumeric Gnumeric spreadsheet Gnumeric-kalkultabelo hoja de cálculo de Gnumeric Gnumeric kalkulu-orria Gnumeric-taulukko Gnumeric rokniark feuille de calcul Gnumeric scarbhileog Gnumeric folla de cálculo de Gnumeric גליון עבודה Gnumeric Gnumeric proračunska tablica Gnumeric-munkafüzet Folio de calculo Gnumeric Lembar sebar Gnumeric Foglio di calcolo Gnumeric Gnumeric スプレッドシート Gnumeric электрондық кестесі Gnumeric 스프레드시트 Gnumeric skaičialentė Gnumeric izklājlapa Hamparan Gnumeric Gnumeric-regneark Gnumeric-rekenblad Gnumeric-rekneark fuèlh de calcul Gnumeric Arkusz Gnumeric folha de cálculo Gnumeric Planilha do Gnumeric Foaie de calcul Gnumeric электронная таблица Gnumeric Zošit Gnumeric Razpredelnica Gnumeric Fletë llogaritjesh Gnumeric табела Гномовог бројевника Gnumeric-kalkylblad Gnumeric çalışma sayfası ел. таблиця Gnumeric Bảng tính Gnumeric. Gnumeric 工作簿 Gnumeric 試算表 Gnuplot document مستند Gnuplot Dakument Gnuplot Документ — Gnuplot document gnuplot dokument Gnuplot Gnuplotdokument Gnuplot-Dokument Έγγραφο Gnuplot Gnuplot document Gnuplot-dokumento documento de Gnuplot Gnuplot dokumentua Gnuplot-asiakirja Gnuplot skjal document Gnuplot cáipéis Gnuplot documento de Gnuplot מסמך Gnuplot Gnuplot dokument Gnuplot dokumentum Documento Gnuplot Dokumen Gnuplot Documento Gnuplot Gnuplot ドキュメント Gnuplot құжаты Gnuplot 문서 Gnuplot dokumentas Gnuplot dokuments Gnuplot-dokument Gnuplot-document Gnuplot-dokument document Gnuplot Dokument Gnuplot documento Gnuplot Documento do Gnuplot Document Gnuplot документ Gnuplot Dokument Gnuplot Dokument Gnuplot Dokument Gnuplot документ Гнуплота Gnuplot-dokument Gnuplot belgesi документ Gnuplot Tài liệu Gnuplot Gnuplot 文档 Gnuplot 文件 Graphite scientific graph مبيان الجرافيت العلمي Navukovy hrafik Graphite Графика — Graphite gràfic científic Graphite vědecký graf Graphite Graphite videnskabelig graf Wissenschaftlicher Graphite-Graph Επιστημονικό γράφημα Graphite Graphite scientific graph scienca grafikaĵo de Graphite gráfico científico de Graphite Graphite - grafiko zientifikoak Graphite- tieteellinen graafi Grapite vísindarlig ritmynd graphe Graphite scientific graf eolaíoch Graphite gráfica científica de Graphite תרשים מדעי של Graphite Graphite znanstveni grafikon Graphite tudományos grafikon Graphico scientific Graphite Grafik sains Graphite Grafico scientifico Graphite Graphite scientific グラフ Graphite ғылыми кескіні Graphite 과학 그래프 Graphite mokslinė diagrama Graphite zinātniskais grafiks Graf saintifik Graphite Vitenskapelig graf fra Graphite Graphite wetenschappelijke grafiek Graphite vitskaplege graf graphe Graphite scientific Wykres naukowy Graphite gráfico científico Graphite Gráfico científico do Graphite Grafic științific Graphite научная диаграмма Graphite Vedecký graf Graphite Datoteka znanstvenega grafa Graphite Grafik shkencor Graphite Графитов научни графикони Vetenskaplig Graphite-grafer Graphite bilimsel grafiği наукова графіка Graphite Biểu đồ khoa học Graphite Graphite 科学图形 Graphite 科學圖表 GTKtalog catalog كتالوج GTKtalog Kataloh GTKtalog Каталог — Gtktalog catàleg de GTKtalog katalog GTKtalog GTKtalog-katalog GTKtalog-Katalog Κατάλογος GTKtalog GTKtalog catalogue katalogo de GTKtalog catálogo de GTKtalog Gtktalog katalogoa GTKtalog-luettelo GTKtalog skrá catalogue Gtktalog catalóg GTKtalog catálogo de GTKtalog קטלוג GTKtalog GTKtalog katalog GTKtalog-katalógus Catalogo GTKtalog Katalog GTKtalog Catalogo GTKtalog GTKtalog カタログ GTKtalog-ის კატალოგი GTKtalog каталогы GTKtalog 카탈로그 GTKtalog katalogas GTKtalog katalogs Katalog GTKtalog GTKtalog-katalog GTKtalog-catalogus GTKtalog-katalog catalòg Gtktalog Katalog programu GTKtalog catálogo GTKtalog Catálogo GTKtalog Catalog GTKalog каталог GTKtalog Katalóg GTKtalog Datoteka kataloga GTKtalog Katallog GTKtalog каталог ГТКталога GTKtalog-katalog Gtktalog kataloğu каталог GTKtalog Phân loại GTKtalog GTKtalog 目录 GTKtalog 光碟目錄 TeX DVI document (gzip-compressed) مستند TeX DVI (مضغوط-gzip) Dakument TeX DVI (gzip-skampresavany) Документ — TeX DVI, компресиран с gzip document DVI de TeX (amb compressió gzip) dokument TeX DVI (komprimovaný pomocí gzip) TeX DVI-dokument (gzip-komprimeret) TeX-DVI-Dokument (gzip-komprimiert) Έγγραφο TeX DVI (συμπιεσμένο με gzip) TeX DVI document (gzip-compressed) documento DVI de TeX (comprimido con gzip) TeX DVI dokumentua (gzip-ekin konprimitua) TeX DVI -asiakirja (gzip-pakattu) TeX DVI skjal (gzip-stappað) document DVI TeX (compressé gzip) cáipéis DVI TeX (comhbhrúite le gzip) documento DVI de TeX (comprimido con gzip) מסמך מסוג TeX DVI (מכווץ ע״י gzip) TeX DVI dokument (komprimiran gzip-om) TeX DVI dokumentum (gzip-pel tömörítve) Documento TeX DVI (comprimite con gzip) Dokumen TeX DVI (terkompresi gzip) Documento Tex DVI (compresso con gzip) Tex DVI ドキュメント (gzip 圧縮) TeX DVI құжаты (gzip-пен сығылған) TeX DVI 문서(GZIP 압축) TeX DVI dokumentas (suglaudintas su gzip) TeX DVI dokuments (saspiests ar gzip) TeX DVI-dokument (gzip-komprimert) TeX DVI-document (ingepakt met gzip) TeX DVI-dokument (pakka med gzip) document DVI TeX (compressat gzip) Dokument TeX DVI (kompresja gzip) documento TeX DVI (compressão gzip) Documento DVI TeX (compactado com gzip) Document TeX DVI (comprimat gzip) документ TeX DVI (сжатый gzip) Dokument TeX DVI (komprimovaný pomocou gzip) Dokument TeX DVI (stisnjen z gzip) Dokument TeX DVI (i kompresuar me gzip) ТеКс ДВИ документ (запакован гзип-ом) TeX DVI-dokument (gzip-komprimerat) TeX DVI belgesi (gzip ile sıkıştırılmış) документ TeX DVI (стиснений gzip) Tài liệu DVI TeX (đã nén gzip) TeX DVI 文档(gzip 压缩) TeX DVI 文件 (gzip 格式壓縮) Gzip archive أرشيف Gzip Archiŭ gzip Архив — gzip arxiu gzip archiv gzip Gzip-arkiv Gzip-Archiv Συμπιεσμένο αρχείο Gzip Gzip archive Gzip-arkivo archivador Gzip Gzip artxiboa Gzip-arkisto Gzip skjalasavn archive gzip cartlann Gzip arquivo Gzip ארכיון Gzip Gzip arhiva Gzip archívum Archivo Gzip Arsip Gzip Archivio gzip Gzip アーカイブ Gzip архиві GZIP 압축 파일 Gzip archyvas Gzip arhīvs Gzip-arkiv Gzip-archief Gzip-arkiv archiu gzip Archiwum gzip arquivo Gzip Pacote Gzip Arhivă Gzip архив GZIP Archív gzip Datoteka arhiva Gzip Arkiv gzip Гзип архива Gzip-arkiv Gzip arşivi архів gzip Kho nén gzip Gzip 归档文件 Gzip 封存檔 PDF document (gzip-compressed) مستند PDF (مضغوط-gzip) Dakument PDF (gzip-skampresavany) Документ — PDF, компресиран с gzip document PDF (amb compressió gzip) dokument PDF (komprimovaný pomocí gzip) PDF-dokument (gzip-komprimeret) PDF-Dokument (gzip-komprimiert) Έγγραφο PDF (συμπιεσμένο με gzip) PDF document (gzip-compressed) documento PDF (comprimido con gzip) PDF dokumentua (gzip-ekin konprimitua) PDF-asiakirja (gzip-pakattu) PDF skjal (gzip-stappað) document PDF (compressé gzip) cáipéis PDF (comhbhrúite le gzip) documento PDF (comprimido en gzip) מסמך PDF (מכווץ ע״י gzip) PDF dokument (gzip sažet) PDF dokumentum (gzip-tömörítésű) Documento PDF (comprimite con gzip) Dokumen PDF (terkompresi gzip) Documento PDF (compresso con gzip) PDF ドキュメント (gzip 圧縮) PDF құжаты (gzip-пен сығылған) PDF 문서(GZIP 압축) PDF dokumentas (suglaudintas su gzip) PDF dokuments (saspiests ar gzip) PDF-dokument (gzip-komprimert) PDF-document (ingepakt met gzip) PDF-dokument (pakka med gzip) document PDF (compressat gzip) Dokument PDF (kompresja gzip) documento PDF (compressão gzip) Documento PDF (compactado com gzip) Document PDF (comprimat gzip) документ PDF (сжатый gzip) Dokument PDF (komprimovaný pomocou gzip) Dokument PDF (stisnjen z gzip) Dokument PDF (i kompresuar me gzip) ПДФ документ (запакован гзип-ом) PDF-dokument (gzip-komprimerat) PDF belgesi (gzip ile sıkıştırılmış) документ PDF (стиснений gzip) Tài liệu PDF (đã nén gzip) PDF 文档(gzip 压缩) PDF 文件 (gzip 格式壓縮) PostScript document (gzip-compressed) مستند PostScript (مضغوط-gzip) Dakument PostScript (gzip-skampresavany) Документ — PostScript, компресиран с gzip document PostScript (amb compressió gzip) dokument PostScript (komprimovaný pomocí gzip) PostScript-dokument (gzip-komprimeret) PostScript-Dokument (gzip-komprimiert) Έγγραφο PostScript (συμπιεσμένο με gzip) PostScript document (gzip-compressed) PostScript-dokumento (kunpremita per gzip) documento PostScript (comprimido con gzip) PostScript dokumentua (gzip-konprimitua) PostScript-asiakirja (gzip-pakattu) PostScript skjal (gzip-stappað) document PostScript (compressé gzip) cáipéis PostScript (comhbhrúite le gzip) documento PostScript (comprimido con gzip) מסמך PostScript (מכוות ע״י gzip) PostScript dokument (gzip sažet) PostScript-dokumentum (gzip-pel tömörítve) Documento PostScript (comprimite con gzip) Dokumen PostScript (terkompresi gzip) Documento PostScript (compresso con gzip) PostScript ドキュメント (gzip 圧縮) PostScript құжаты (gzip-пен сығылған) PostScript 문서(GZIP 압축) PostScript dokumentas (suglaudintas su gzip) PostScript dokuments (saspiests ar gzip) Dokumen PostScript (dimampatkan-gzip) PostScript-dokument (gzip-komprimert) PostScript-document (ingepakt met gzip) PostScript-dokument (pakka med gzip) document PostEscript (compressat gzip) Dokument Postscript (kompresja gzip) documento PostScript (compressão gzip) Documento PostScript (compactado com gzip) Document PostScript (comprimat gzip) документ PostScript (сжатый gzip) Dokument PostScript (komprimovaný pomocou gzip) Dokument PostScript (stisnjen z gzip) Dokument PostScript (i kompresuar me gzip) Постскрипт документ (запакован гзип-ом) Postscript-dokument (gzip-komprimerat) PostScript belgesi (gzip ile sıkıştırılmış) документ PostScript (стиснене gzip) Tài liệu PostScript (đã nén gzip) PostScript 文档(gzip 压缩) PostScript 文件 (gzip 格式壓縮) HDF document مستند HDF HDF sənədi Dakument HDF Документ — HDF document HDF dokument HDF Dogfen HDF HDF-dokument HDF-Dokument Έγγραφο HDF HDF document HDF-dokumento documento HDF HDF dokumentua HDF-asiakirja HDF skjal document HDF cáipéis HDF documento HDF מסמך HDF HDF dokument HDF-dokumentum Documento HDF Dokumen HDF Documento HDF HDF ドキュメント HDF құжаты HDF 문서 HDF dokumentas HDF dokuments Dokumen HDF HDF-dokument HDF-document HDF-dokument document HDF Dokument HDF documento HDF Documento HDF Document HDF документ HDF Dokument HDF Dokument HDF Dokument HDF ХДФ документ HDF-dokument HDF belgesi документ HDF Tài liệu HDF HDF 文档 HDF 文件 HDF Hierarchical Data Format IFF file fitxer IFF soubor IFF IFF-fil IFF-Datei Αρχείο IFF IFF file archivo IFF IFF fitxtegia IFF-tiedosto fichier IFF Ficheiro IFF קובץ IFF IFF datoteka IFF fájl File IFF Berkas IFF File IFF IFF ファイル IFF файлы IFF 파일 IFF datne fichièr IFF Plik IFF ficheiro IFF Arquivo IFF файл IFF Súbor IFF Datoteka IFF ИФФ датотека IFF-fil IFF dosyası файл IFF IFF 文件 IFF 檔案 IFF Interchange File Format iPod firmware برنامج عتاد الـiPod Firmware iPod Фърмуер за iPod microprogramari d'iPod firmware iPod iPod-styreprogram iPod-Firmware Υλικολογισμικό iPod iPod firmware iPod-mikroprogramaro firmware de iPod iPod firmwarea iPod-laiteohjelmisto iPod fastbúnaður firmware iPod dochtearraí iPod firmware de iPod קושחת ipod iPod firmver iPod-firmware Firmware iPod peranti tegar iPod Firmware iPod iPod ファームウェア iPod микробағдарламасы iPod 펌웨어 iPod programinė įranga iPod aparātprogrammatūra Firmware iPod iPod-firmware iPod-firmware iPod-firmvare firmware iPod Oprogramowanie wewnętrzne iPod firmware iPod Firmware do iPod Firmware iPod микропрограмма iPod Firmware iPod Programska strojna oprema iPod Firmware iPod ајПод-ов уграђени fast iPod-program iPod üretici yazılımı мікропрограма iPod phần vững iPod iPod 固件 iPod 韌體 Java archive أرشيف Java Archiŭ Java Архив — Java arxiu de Java archiv Java Javaarkiv Java-Archiv Συμπιεσμένο αρχείο Java Java archive Java-arkivo archivador Java Java artxiboa Java-arkisto Java skjalasavn archive Java cartlann Java arquivo Java ארכיון Java Java arhiva Java-archívum Archivo Java Arsip Java Archivio Java Java アーカイブ Java архиві Java 묶음 파일 Java archyvas Java arhīvs Arkib Java Java-arkiv Java-archief Java-arkiv archiu Java Archiwum Java arquivo Java Pacote Java Arhivă Java архив Java Archív Java Datoteka arhiva Java Arkiv Java архива Јаве Java-arkiv Java arşivi архів Java Kho nén Java Java 归档文件 Java 封存檔 Java class صنف java Klasa Java Клас на Java classe de Java třída Java Javaklasse Java-Klasse Κλάση Java Java class Java-klaso clase de Java Java-ko klasea Java-luokka Java flokkur classe Java aicme Java clase de Java מחלקת Java Java klasa Java-osztály Classe Java Kelas Java Classe Java Java クラス Java класы Java 클래스 Java klasė Java klase Kelas Java Java-klasse Java-klasse Java-klasse classa Java Klasa Java classe Java Classe Java Clasă Java класс Java Trieda Java Datoteka razreda Java Klasë Java разред Јаве Java-klass Java sınıfı клас Java Hạng Java Java 类 Java class JNLP file ملف JNLP Fajł JNLP Файл — JNLP fitxer JNLP soubor JNLP JNPL-fil JNLP-Datei Αρχείο JNLP JNLP file JNLP-dosiero archivo JNPL JNLP fitxategia JNLP-tiedosto JNLP fíla fichier JNLP comhad JNLP ficheiro JNLP קובץ JNLP JNLP datoteka JNLP fájl File JNLP Berkas JNLP File JNPL JNLP ファイル JNLP файлы JNLP 파일 JNLP failas JNLP datne JNLP-fil JNLP-bestand JNLP-fil fichièr JNLP Plik JNLP ficheiro JNLP Arquivo JNLP Fișier JNLP файл JNLP Súbor JNLP Datoteka JNLP File JNLP ЈНЛП датотека JNLP-fil JNLP dosyası файл JNLP Tập tin JNLP JNLP 文件 JNLP 檔案 JNLP Java Network Launching Protocol Java keystore مخزن مفاتيح جافا Ключодържател — Java magatzem de claus de Java úložiště klíčů Java Javanøglelager Java-Schlüsselbund Χώρος αποθήκευσης κλειδιών Java Java keystore almacén de claves de Java Java-ren gako-biltegia Java-avainvarasto Java lyklagoymsla stockage de clés Java eochairstór Java almacén de chaves de Java הקשת מקלדת של Java Java baza ključeva Java kulcstároló Magazin de claves Java Penyimpanan kunci Java Keystore Java Java キーストア Java сақталымы Java 키 저장소 Java raktų saugykla Java keystore Java keystore emmagazinatge de claus Java Baza kluczy Java armazém de chaves Java Keystore de Java Stocare chei Java хранилище ключей Java Úložisko kľúčov Java Datoteka tipkovne razporeditve Java смештај кључа Јаве Java-nyckellager Java deposu сховище ключів Java Java 密钥库 Java 金鑰儲存 Java JCE keystore مخزن مفاتيح Java JCE Ключодържател — Java JCE magatzem de claus JCE de Java úložiště klíčů Java JCE Java JCE-nøglelager Java JCE-Schlüsselbund Αποθήκη κλειδιών Java JCE Java JCE keystore almacén de claves JCE de Java Java JCE-ren gako-biltegia Java JCE -avainvarasto Java JCE lyklagoymsla stockage de clés Java JCE eochairstór Java JCE almacén de chves JCE de Java הקשה מסוג Java JCE Java JCE baza ključeva Java JCE kulcstároló Magazin de claves Java JCE Penyimpanan kunci Java JCE Keystore Java JCE Java JCE キーストア Java JCE сақталымы Java JCE 키 저장소 Java JCE raktų saugykla Java JCE keystore Java JCE keystore emmagazinatge de claus Java JCE Baza kluczy Java JCE armazém de chaves JavaJCE Keystore JCE do Java Stocare chei Java JCE хранилище ключей Java JCE Úložisko kľúčov Java JCE Datoteka tipkovne razporeditve Java JCE смештај ЈЦЕ кључа Јаве Java JCE-nyckellager Java JCE deposu сховище ключів JCE Java Java JCE 密钥库 Java JCE 金鑰儲存 JCE Java Cryptography Extension Pack200 Java archive أرشيف Pack200 Java Archiŭ Pack200 Java Архив — Java Pack200 arxiu de Java en Pack200 archiv Java Pack200 Pack200 Java-arkiv Pack200-Java-Archiv Συμπιεσμένο αρχείο Java Pack200 Pack200 Java archive archivador Pack200 Java Pack2000 Java artxiboa Pack200-Java-arkisto Pack200 Java skjalasavn archive Java Pack200 cartlann Java Pack200 arquivo Pack200 Java ארכיון מסוג Pack200 Java Pack200 Java arhiva Pack200 Java-archívum Archivo Java Pack200 Arsip Pack200 Java Archivio Pack200 Java Pack200 Java アーカイブ Pack200 Java архиві Pack200 Java 압축 파일 Pack200 Java archyvas Pack200 Java arhīvs Pack200 Java-arkiv Pack200 Java-archief Pack200 Java-arkiv archiu Java Pack200 Archiwum Java Pack200 arquivo Java Pack200 Pacote Java Pack200 Arhivă Java Pack2000 архив Java Pack200 Archív Java Pack200 Datoteka arhiva Pack200 Java Arkiv Java Pack200 архива Јаве Пак200 Pack200 Java-arkiv Pack200 Java arşivi архів Java Pack200 Kho nén Java Pack200 Pack200 Java 归档文件 Pack200 Java 封存檔 JavaScript program برنامج جافاسكربت Prahrama JavaScript Програма на JavaScript programa JavaScript program v JavaScriptu JavaScript-program JavaScript-Programm Πρόγραμμα JavaScript JavaScript program JavaScript-programo programa en JavaScript JavaScript programa JavaScript-ohjelma JavaScript forrit programme JavaScript ríomhchlár JavaScript programa JavaScript תכנית JavaScript JavaScript program JavaScript-program Programma JavaScript Program JavaScript Programma JavaScript JavaScript プログラム JavaScript бағдарламасы JavaScript 프로그램 JavaScript programa JavaScript programma Program JavaScript JavaScript-program JavaScript-programma JavaScript-program programa JavaEscript Pogram JavaScript programa JavaScript Programa JavaScript Program JavaScript сценарий JavaScript Program jazyka JavaScript Programska datoteka JavaScript Program JavaScript програм Јава скрипте JavaScript-program JavaScript programı програма мовою JavaScript Chương trình JavaScript Javascript 程序 JavaScript 程式 JSON document document JSON dokument JSON JSON-dokument JSON-Dokument Έγγραφο JSON JSON document documento JSON JSON dokumentua JSON-asiakirja document JSON Documento JSON מסמך JSON JSON dokument JSON dokumentum Documento JSON Dokumen JSON Documento JSON JSON құжаты JSON 문서 document JSON Dokument JSON documento JSON Documento JSON Документ JSON Dokument JSON Dokument JSON ЈСОН документ JSON-dokument JSON belgesi документ JSON JSON 文档 JSON 文件 JSON JavaScript Object Notation JRD document document JRD dokument JRD JRD-dokument JRD-Dokument Έγγραφο JRD JRD document documento JRD JRD dokumentua JRD-asiakirja JRD dokument JRD dokumentum Documento JRD Dokumen JRD Documento JRD JRD құжаты JRD 문서 document JRD Dokument JRD doxumento JRD Documento JRD документ JRD Dokument JRD ЈРД документ JRD-dokument JRD belgesi документ JRD JRD 文档 JRD 文件 JRD JSON Resource Descriptor JSON patch pedaç de JSON cesta JSON JSON-rettelse JSON-Patch JSON patch parche en JSON JSON zakrpa JSON javítócsomag Patch JSON Patch JSON Patch JSON JSON 패치 correctiu JSON Łata JSON patch JSON Patch JSON изменение JSON Záplata JSON ЈСОН закрпа JSON patch JSON yaması латка JSON JSON 补丁文件 JSON 修補檔 JSON JavaScript Object Notation JSON-LD document document JSON-LD dokument JSON-LD JSON-LD-dokument JSON-LD-Dokument Έγγραφο JSON-LD JSON-LD document documento JSON-LD JSON-LD dokumentua JSON-LD-asiakirja JSON-LD dokument JSON-LD dokumentum Documento JSON-LD Dokumen JSON-LD Documento JSON-LD JSON-LD құжаты JSON-LD 문서 Document JSON-LD Dokument JSON-LD documento JSON-LD Documento JSON-LD документ JSON-LD Dokument JSON-LD ЈСОН-ЛД документ JSON-LD-dokument JSON-LD belgesi документ JSON-LD JSON-LD 文档 JSON-LD 文件 JSON-LD JavaScript Object Notation for Linked Data Jupyter Notebook CoffeeScript document document CoffeeScript dokument CoffeeScript CoffeeScript-dokument CoffeeScript-Dokument Έγγραφο CoffeeScript CoffeeScript document documento en CoffeeScript CoffeeScript dokumentua CoffeeScript-asiakirja CoffeeScript dokument CoffeeScript dokumentum Documento CoffeeScript Dokumen CoffeeScript Documento CoffeeScript CoffeeScript құжаты CoffeeScript 문서 Document CoffeScript Dokument CoffeeScript documento CoffeeScript Documento CoffeeScript документ CoffeeScript Dokument CoffeeScript Кофи скрипт документ CoffeeScript-dokument CoffeeScript belgesi документ CoffeeScript CoffeeScript 文档 CoffeeScript 文件 JBuilder project مشروع JBuilder Prajekt JBuilder Проект — JBuilder projecte de JBuilder projekt JBuilder JBuilder-projekt JBuilder-Projekt Εργο JBuilder JBuilder project JBuilder-projekto proyecto de JBuilder JBuilder proiektua JBuilder-projekti JBuilder verkætlan projet JBuilder tionscadal JBuilder proxecto de JBuilder מיזם JBuilder JBuilder projekt JBuilder-projekt Projecto JBuilder Proyek JBuilder Progetto JBuilder JBuilder プロジェクト JBuilder жобасы JBuilder 프로젝트 JBuilder projektas JBuilder projekts Projek JBuilder JBuilder-prosjekt JBuilder-project JBuilder-prosjekt projècte JBuilder Projekt JBuilder projecto JBuilder Projeto do JBuilder Proiect JBuilder проект JBuilder Projekt JBuilder Datoteka projekta JBuilder Projekt JBuilder пројекат ЈГрадитеља JBuilder-projekt JBuilder projesi проект JBuilder Dự án JBuilder JBuilder 工程 JBuilder 專案 Karbon14 drawing تصميم Karbon14 Rysunak Karbon14 Чертеж — Karbon14 dibuix de Karbon14 kresba Karbon14 Karbon14-tegning Karbon14-Zeichnung Σχέδιο Karbon14 Karbon14 drawing Karbon14-grafikaĵo dibujo de Karbon14 Karbon14 marrazkia Karbon14-piirros Karbon14 tekning dessin Karbon14 líníocht Karbon14 debuxo de Karbon14 ציור Karbon14 Karbon14 crtež Karbon14-rajz Designo Karbon14 Gambar Karbon14 Disegno Karbon14 Karbon14 ドロー Karbon14 суреті Karbon14 그림 Karbon14 piešinys Karbon14 zīmējums Lukisan Karbon14 Karbon14-tegning Karbon14-tekening Karbon14-teikning dessenh Karbon14 Rysunek Karbon14 desenho Karbon14 Desenho do Karbon14 Desen Karbon14 изображение Karbon14 Kresba Karbon14 Datoteka risbe Karbon14 Vizatim Karbon14 цртеж Карбона14 Karbon14-teckning Karbon14 çizimi малюнок Karbon14 Bản vẽ Karbon14 Karbon14 绘图 Karbon14 繪圖 KChart chart رسم بياني KChart Hrafik KChart Диаграма — KChart diagrama de KChart graf Chart KChart-diagram KChart-Diagramm Γράφημα KChart KChart chart KChart-diagramo gráfico de KChart KChart diagrama KChart-kaavio KChart strikumynd graphique KChart cairt KChart gráfica de KChart תרשים KChart KChart grafikon KChart-grafikon Graphico KChart Bagan KChart Grafico KChart KChart チャート KChart диаграммасы KChart 차트 KChart diagrama KChart diagramma Carta KChart KChart-graf KChart-grafiek KChart-diagram grafic KChart Wykres KChart gráfico KChart Gráfico do KChart Diagramă KChart диаграмма KChart Graf KChart Datoteka grafikona KChart Grafik KChart графикон К-графика KChart-diagram KChart çizgesi діаграма KChart Sơ đồ KChart KChart 图表 KChart 圖表 Kexi settings for database server connection إعدادات Kexi للإتصال بخادم قاعدة البيانات Връзка към база от данни — Kexi ajusts de Kexi per a la connexió al servidor de bases de dades nastavení Kexi ke spojení s databázovým serverem Kexiopsætning til forbindelsen for databaseserveren Kexi-Einstellungen für Verbindung zum Datenbankserver Ρυθμίσεις Kexi για σύνδεση με εξυπηρετητή βάσεων δεδομένων Kexi settings for database server connection configuración de Kexi para conectar con un servidor de bases de datos Kexi-ren ezarpenak datu-basearen zerbitzariarekin konektatzeko Kexi-tietokantayhteysasetukset Kexi stillingar fyri dátustovnsambætara sambinding paramètres Kexi pour connexion au serveur de base de données socruithe Kexi do cheangal le freastalaí bunachair sonraí configuración de Kexi para conexión con servidor de base de datos הגדרות של Kexi עבור חיבור שרת למסד נתונים Kexi postavke za povezeivanje baza podataka poslužitelja Kexi beállítások adatbáziskiszolgáló-kapcsolathoz Configuration Kexi pro connexion al servitor de base de datos Tatanan Kexi bagi koneksi server basis data Impostazioni Kexi per connessione a server di database データベースサーバ接続用の Kexi 設定 Дерекқор серверге байланыс Kexi баптаулары Kexi 데이터베이스 서버 연결 설정 Kexi duomenų bazės ryšio su serveriu parametrai Kexi iestatījumi datubāzes servera savienojumam Kexi instellingen voor database server connectie paramètres Kexi per connexion al servidor de banca de donadas Ustawienia Kexi dla połączenia serwera bazy danych definições Kexi para ligação de servidor de base de dados Configurações do Kexi para conexão a servidor de banco de dados Configurări Kexi pentru conexiunea la serverul de baze de date параметры Kexi для подключения к серверу БД Nastavenia Kexi pre pripojenie k databázovému serveru Strežniška povezava do nastavitvene datoteke Kexi. подешавања Кексија за везу са сервером базе података Kexi-inställningar för anslutning till databasserver Veritabanı sunucu bağlantısı için Kexi ayarları параметри Kexi для встановлення з’єднання з сервером бази даних Kexi 数据库服务器连接设置 Kexi 設定值 (資料庫伺服器連線用) shortcut to Kexi project on database server اختصار لمشروع Kexi على خادم قاعدة بيانات Връзка към проект — Kexi drecera al projecte de Kexi en un servidor de base de dades zástupce projektu Kexi na databázovém serveru genvej til Kexiprojekt på databaseserver Schnellzugriff zum Kexi-Projekt auf dem Datenbankserver Συντόμευση σε έργο Kexi στον εξυπηρετητή βάσης δεδομένων shortcut to Kexi project on database server acceso directo a proyecto Kexi en el servidor de bases de datos lasterbidea datu-basearen zerbitzariko Kexi proiekturako pikakuvake tietokantapalvelimella olevaan Kexi-projektiin snarvegur til Kexi verkætlan á dátustovnsambætara raccourci vers projet Kexi sur serveur de base de données aicearra go tionscadal Kexi ar fhreastalaí bunachair sonraí acceso directo a proxecto Kexi no servidor de bases de datos קיצור דרך לפרוירט Kexi בשרת נתונים prečac za Kexi projekt na poslužitelju baze podataka indítóikon adatbázis-kiszolgálón lévő Kexi projektre Ligamine a projecto Kexi in servitor de base de datos pintasan ke projek Kexi pada server basis data Scorciatoia a progetto Kexi su server di database データベースサーバの Kexi プロジェクトへのショートカット дерекқор серверіндегі Kexi жобасына сілтеме 데이터베이스 서버의 Kexi 프로젝트 바로 가기 nuoroda į Kexi projektą duomenų bazės serveryje īsceļš uz Kexi projektu datubāzes serverī shortcut naar Kexi project op database server acorchi cap a projècte Kexi sus servidor de banca de donadas Skrót do projektu Kexi na serwerze bazy danych atalho para projeto Kexi em servidor de base de dados Atalho para projeto Kexi no servidor de banco de dados scurtătură către un proiect Kexi pe un server de baze de date ссылка на проект Kexi на сервере БД Zástupca projektu Kexi na databázovom serveri bližnjica do Kexi projekta na podatkovnem strežniku пречица до пројекта Кексија на серверу базе података genväg till Kexi-projekt på databasserver veritabanı üzerindeki Kexi projesine kısayol скорочення для проекту Kexi на сервері бази даних 数据库服务器上 Kexi 项目的快捷方式 資料庫伺服器上 Kexi 專案的捷徑 Kexi database file-based project مشروع قاعدة بيانات Kexi يعتمد على ملفات Проект с база от данни — Kexi projecte basat en fitxer de base de dades de Kexi projekt založený na souboru databáze Kexi Filbaseret projekt for Kexidatabase Dateibasiertes Kexi-Datenbankprojekt Έργο βάσης δεδομένων Kexi βασισμένο σε αρχεία Kexi database file-based project proyecto de base de datos basada en archivos de Kexi Kexi datu-baseko fitxategian oinarritutako proiektua Kexin tiedostoperustainen tietokantaprojekti Kexi dátustovns fílugrundað verkætlan projet de base de données Kexi en mode fichier tionscadal bunachair sonraí Kexi bunaithe ar chomhaid proxecto baseado no ficheiro-base de datos Kexi מיזם מסד נתונים מבוסס-קובץ של Kexi Kexi baza podataka datotekom temeljen projekt Kexi adatbázisfájl-alapú projekt Projecto de base de datos Kexi in modo file Projek berbasis berkas basis data Kexi Progetto su file di database Kexi Kexi データベース ファイルベースプロジェクト Файл негізінде жоба үшін Kexi дерекқоры Kexi 데이터베이스 파일 기반 프로젝트 Kexi duomenų bazės failo tipo projektas Kexi datubāzes datnes balstīts projekts Kexi database bestandgebaseerd project projècte de banca de donadas Kexi en mòde fichièr Projekt bazy danych Kexi oparty na pliku projeto Kexi em base de dados baseada em ficheiros Projeto de banco de dados baseado em arquivo do Kexi Proiect bazat pe fișiere al bazei de date Kexi файловый проект базы данных Kexi Projekt databázy Kexi s úložiskom typu súbor Datoteka projekta podatkovne zbirke Kexi пројекат Кексијеве базе података на основу датотеке Kexi-databas för filbaserat projekt Dosya temelli Kexi veritabanı projesi проект файлової бази даних Kexi Kexi 基于文件的数据库项目 Kexi 資料庫檔案基礎專案 Kexi database file-based project مشروع قاعدة بيانات Kexi يعتمد على ملفات Проект с база от данни — Kexi projecte basat en fitxer de base de dades de Kexi projekt založený na souboru databáze Kexi Filbaseret projekt for Kexidatabase Dateibasiertes Kexi-Datenbankprojekt Έργο βάσης δεδομένων Kexi βασισμένο σε αρχεία Kexi database file-based project proyecto de base de datos basada en archivos de Kexi Kexi datu-baseko fitxategian oinarritutako proiektua Kexin tiedostoperustainen tietokantaprojekti Kexi dátustovns fílugrundað verkætlan projet de base de données Kexi en mode fichier tionscadal bunachair sonraí Kexi bunaithe ar chomhaid proxecto baseado no ficheiro-base de datos Kexi מיזם מסד נתונים מבוסס-קובץ של Kexi Kexi baza podataka datotekom temeljen projekt Kexi adatbázisfájl-alapú projekt Projecto de base de datos Kexi in modo file Projek berbasis berkas basis data Kexi Progetto su file di database Kexi Kexi データベース ファイルベースプロジェクト Файл негізінде жоба үшін Kexi дерекқоры Kexi 데이터베이스 파일 기반 프로젝트 Kexi duomenų bazės failo tipo projektas Kexi datubāzes datnes balstīts projekts Kexi database bestandgebaseerd project projècte de banca de donadas Kexi en mòde fichièr Projekt bazy danych Kexi oparty na pliku projeto Kexi em base de dados baseada em ficheiros Projeto de banco de dados baseado em arquivo do Kexi Proiect bazat pe fișiere al bazei de date Kexi файловый проект базы данных Kexi Projekt databázy Kexi s úložiskom typu súbor Datoteka projekta podatkovne zbirke Kexi пројекат Кексијеве базе података на основу датотеке Kexi-databas för filbaserat projekt Dosya temelli Kexi veritabanı projesi проект файлової бази даних Kexi Kexi 基于文件的数据库项目 Kexi 資料庫檔案基礎專案 KFormula formula صيغة KFormula Formuła KFormula Формула — KFormula fórmula de KFormula vzorec KFormula KFormula-formel KFormula-Formel Μαθηματικός τύπος KFormula KFormula formula KFormula-formulo fórmula de KFormula KFormula formula KFormula-kaava KFormula frymil formule KFormula foirmle KFormula fórmula de KFormula נוסחת KFormula KFormula formula KFormula-képlet Formula KFormula Formula KFormula Formula KFormula KFormula 計算式 KFormula формуласы KFormula 수식 KFormula formulė KFormula formula Formula KFormula KFormula-formel KFormula-formule KFormula-formel formula KFormula Formuła KFormula fórmula KFormula Fórmula do KFormula Formulă KFormula формула KFormula Vzorec KFormula Datoteka formule KFormula Formulë KFormula формула К-формуле KFormula-formel KFormula formülü формула KFormula Công thức KFormula KFormula 公式 KFormula 公式 KIllustrator drawing تصميم KIllustrator Rysunak KIllustrator Чертеж — KIllustrator dibuix de KIllustrator kresba KIllustrator KIllustrator-tegning KIllustrator-Zeichnung Σχέδιο KIllustrator KIllustrator drawing KIllustrator-grafikaĵo dibujo de KIllustrator KIllustrator marrazkia KIllustrator-piirros KIllustrator tekning dessin KIllustrator líníocht KIllustrator debuxo de KIllustrator ציור KIllustrator KIllustrator crtež KIllustrator-rajz Designo KIllustrator Gambar KIllustrator Disegno KIllustrator KIllustrator ドロー KIllustrator суреті KIllustrator 그림 KIllustrator piešinys KIllustrator zīmējums Lukisan KIllustrator KIllustrator-tegning KIllustrator-tekening KIllustrator-teikning dessenh KIllustrator Rysunek KIllustrator desenho KIllustrator Desenho do KIllustrator Desen KIllustrator изображение KIllustrator Kresba KIllustrator Datoteka risbe KIllustrator Vizatim KIllustrator цртеж К-илустратора KIllustrator-teckning KIllustrator çizimi малюнок KIllustrator Bản vẽ KIllustrator KIllustrator 绘图 KIllustrator 繪圖 Kivio flowchart قائمة تدفق Kivio Blok-schiema Kivio Диаграма — Kivio diagrama de flux de Kivio vývojový diagram Kivio Kiviorutediagram Kivio-Flussdiagramm Διάγραμμα ροής Kivio Kivio flowchart Kivo-fluskemo diagrama de flujo de Kivio Kivio diagrama Kivio-vuokaavio Kivio leiðarit diagramme de flux Kivio sreabhchairt Kivio gráfica de fluxo de Kivio תרשים זרימה של Kivio Kivio dijagram toka Kivio-folyamatábra Diagramma de fluxo Kivio Bagan Kivio Diagramma di flusso Kivio Kivio フローチャート Kivio диаграммасы Kivio 순서도 Kivio eigos diagrama Kivio blokshēma Cartalir Kivio Kivio-flytdiagram Kivio-stroomschema Kivio-flytdiagram diagrama de flux Kivio Diagram przepływów Kivio gráfico de fluxo Kivio Fluxograma do Kivio Diagramă Kivio диаграмма Kivio Vývojový diagram Kivio Datoteka grafikona Kivio Diagramë fluksi Kivio Кливиов дијаграм протока Kivio-flödesschema Kivio akış şeması блок-схема Kivio Lược đồ Kivio Kivio 流程图 Kivio 圖表 Kontour drawing تصميم Kontour Rysunak Kontour Чертеж — Kontour dibuix de Kontour kresba Kontour Kontourtegning Kontour-Zeichnung Σχέδιο Kontour Kontour drawing Kontour-grafikaĵo dibujo de Kontour Kontour marrazkia Kontour-piirros Kontour tekning dessin Kontour líníocht Kontour debuxo de Kontour ציור Kontour Kontour crtež Kontour-rajz Designo Kontour Gambar Kontour Disegno Kontour Kontour ドロー Kontour суреті Kontour 그림 Kontour piešinys Kontour zīmējums Lukisan Kontour Kontour-tegning Kontour-tekening Kontour-teikning dessenh Kontour Rysunek Kontour desenho Kontour Desenho do Kontour Desen Kontour изображение Kontour Kresba Kontour Datoteka risbe Kontour Vizatim Kontour Контуров цртеж Kontour-teckning Kontour çizimi малюнок Kontour Bản vẽ Kontour Kontour 绘图 Kontour 繪圖 KPovModeler scene مشهد KPovModeler Scena KPovModeler Сцена — KPovModeler escena de KPovModeler scéna KPovModeler KPovModeler-scene KPovModeler-Szene Σκηνή KPovModeler KPovModeler scene KPovModeler-sceno escena de KPovModeler KPovModeler eszena KPovModeler-näkymä KPovModeler leikmynd scène KPovModeler radharc KPovModeler escena de KPovModeler סצנת KPovModeler KPovModeler scena KPovModeler-jelenet Scena KPovModeler Scene KPovModeler Scena KPovModeler KPovModeler シーン KPovModeler сахнасы KPovModeler 장면 KPovModeler scena KPovModeler aina Babak KPovModeler KPovModeler-scene KPovModeler-scène KPovModeler-scene scène KPovModeler Scena KPovModeler cenário KPovModeler Cena do KPovModeler Scenă KPovModeler сцена KPovModeler Scéna KPovModeler Datoteka scene KPovModeler Skenë KPovModeler сцена КПов Моделера KPovModeler-scen KPovModeler sahnesi сцена KPovModeler Cảnh KPovModeler KPovModeler 场景 KPovModeler 場景 KPresenter presentation عرض تقديمي KPresenter Prezentacyja KPresenter Презентация — KPresenter presentació de KPresenter prezentace KPresenter KPresenter-præsentation KPresenter-Präsentation Παρουσίαση KPresenter KPresenter presentation KPresenter-prezentaĵo presentación de KPresenter Kpresenter aurkezpena KPresenter-esitys KPresenter framløga présentation KPresenter láithreoireacht KPresenter presentación de KPresenter מצגת KPresenter KPresenter prezentacija KPresenter-bemutató Presentation KPresenter Presentasi KPresenter Presentazione KPresenter KPresenter プレゼンテーション KPresenter презентациясы KPresenter 프레젠테이션 KPresenter pateiktis KPresenter prezentācija Persembahan Kpresenter KPresenter-presentasjon KPresenter-presentatie KPresenter-presentasjon presentacion KPresenter Prezentacja KPresenter apresentação KPresenter Apresentação do KPresenter Prezentare KPresenter презентация KPresenter Prezentácia KPresenter Predstavitev KPresenter Prezantim i KPresenter презентација К-представљача KPresenter-presentation KPresenter sunum dosyası презентація KPresenter Trình diễn KPresenter KPresenter 演示文稿 KPresenter 簡報檔 Krita document مستند Krita Dakument Krita Документ — Krita document Krita dokument Krita Kritadokument Krita-Dokument Έγγραφο Krita Krita document Krita-dokumento documento de Krita Krita dokumentua Krita-asiakirja Krita skjal document Krita cáipéis Krita documento de Krita מסמך Krita Krita dokument Krita-dokumentum Documento Krita Dokumen Krita Documento Krita Krita ドキュメント Krita құжаты Krita 문서 Krita dokumentas Krita dokuments Dokumen Krita Krita-dokument Krita-document Krita-dokument document Krita Dokument Krita documento Krita Documento do Krita Document Krita документ Krita Dokument Krita Dokument Krita Dokument Krita документ Крите Krita-dokument Krita belgesi документ Krita Tài liệu Krita Krita 文档 Krita 文件 KSpread spreadsheet جدول KSpread Raźlikovy arkuš KSpread Таблица — KSpread full de càlcul de KSpread sešit KSpread KSpread-regneark KSpread-Tabelle Λογιστικό φύλλο KSpread KSpread spreadsheet KSpread-kalkultabelo hoja de cálculo de KSpread KSpread kalkulu-orria KSpread-taulukko KSpread rokniark feuille de calcul KSpread scarbhileog KSpread folla de cálculo de KSpread גליון נתונים של Kspread KSpread proračunska tablica KSpread-munkafüzet Folio de calculo KSpread Lembar sebar KSpread Foglio di calcolo KSpread KSpread スプレッドシート KSpread электрондық кестесі KSpread 스프레드시트 KSpread skaičialentė KSpread izklājlapa Hamparan KSpread KSpread-regneark KSpread-rekenblad KSpread-rekneark fuèlh de calcul KSpread Arkusz KSpread folha de cálculo KSpread Planilha do KSpread Foaie de calcul KSpread электронная таблица KSpread Zošit KSpread Preglednica KSpread Fletë llogaritjesh KSpread табела К-табеле KSpread-kalkylblad KSpread çalışma sayfası ел. таблиця KSpread Bảng tính KSpread KSpread 工作簿 KSpread 試算表 KSpread spreadsheet (encrypted) جدول KSpread (مشفر) Raźlikovy arkuš KSpread (zašyfravany) Таблица — KSpread, шифрирана full de càlcul de KSpread (xifrat) sešit KSpread (šifrovaný) KSpread-regneark (krypteret) KSpread-Tabelle (verschlüsselt) Λογιστικό φύλλο KSpread (κρυπτογραφημένο) KSpread spreadsheet (encrypted) KSpread-kalkultabelo (ĉifrita) hoja de cálculo de KSpread (cifrada) KSpread kalkulu-orria (enkriptatua) KSpread-taulukko (salattu) KSpread rokniark (bronglað) feuille de calcul KSpread (chiffrée) scarbhileog KSpread (criptithe) folla de cálculo de KSpread (cifrada) גליון נתונים של KSpread (מוצפן) KSpread proračunska tablica (šifrirana) KSpread-munkafüzet (titkosított) Folio de calculo KSpread (cryptate) Lembar sebar KSpread (terenkripsi) Foglio di calcolo KSpread (cifrato) KSpread (暗号化) スプレッドシート KSpread электрондық кестесі (шифрленген) 암호화된 KSpread 스프레드시트 KSpread skaičialentė (užšifruota) KSpread izklājlapa (šifrēta) Hampatan KSpread (terenkripsi) KSpread-regneark (kryptert) KSpread-rekenblad (versleuteld) Kryptert KSpread-rekneark fuèlh de calcul KSpread (chifrada) Arkusz KSpread (zaszyfrowany) folha de cálculo KSpread (encriptada) Planilha do KSpread (criptografada) Foaie de calcul KSpread (criptat) электронная таблица KSpread (зашифрованная) Zošit KSpread (šifrovaný) Preglednica KSpread (šifrirana) Fletë llogaritjesh KSpread (e kriptuar) табела К-табеле (шифрована) KSpread-kalkylblad (krypterat) KSpread çalışma sayfası (şifreli) ел. таблиця KSpread (зашифрована) Bảng tính KSpread (đã mật mã) KSpread 加密工作簿 KSpread 試算表 (已加密) KSysV init package حزمة KSysV init Inicyjalny pakunak KSysV Пакет — KSysV init paquet d'inici KSysV balíček init KSysV KSsV init-pakke KSysV-Init-Paket Αρχικό πακέτο KSysV KSysV init package paquete de configuración de init para KSysV KSysV hasieratzeko paketea KSysV init -paketti KSysV init pakki paquet d'initialisation KSysV pacáiste thosú KSysV paquete de KsysV init חבילת KSysV init KSysV init paket KSysV init csomag Pacchetto de initialisation KSysV Paket init KSysV Pacchetto init KSysV KSysV init パッケージ KSysV инициализация дестесі KSysV init 패키지 KSysV init paketas KSysV inicializācijas pakotne KSysV init-pakke KSysV-init-pakket KSysV init-pakke paquet d'initializacion KSysV Pakiet KSysV init pacote inicial KSysV Pacote init do KSysV Pachet KSysV init пакет инициализации KSysV Balíček KSysV init Datoteka paketa KSysV init Paketë init KSysV КСисВ побудни пакет KSysV init-paket KSysV init paketi пакунок KSysV init Gói sở khởi KSysV KSysV init 软件包 KSysV init 封包 Kugar document مستند Kugar Dakument Kugar Документ — Kugar document Kugar dokument Kugar Kugardokument Kugar-Dokument Έγγραφο Kugar Kugar document Kugar-dokumento documento de Kugar Kugar dokumentua Kugar-asiakirja Kugar skjal document Kugar cáipéis Kugar documento de Kugar מסמך Kugar Kugar dokument Kugar-dokumentum Documento Kugar Dokumen Kugar Documento Kugar Kugar ドキュメント Kugar құжаты Kugar 문서 Kugar dokumentas Kugar dokuments Dokumen Kugar Kugar-dokument Kugar-document Kugar-dokument document Kugar Dokument Kuguar documento Kugar Documento do Kugar Document Kugar документ Kugar Dokument Kugar Dokument Kugar Dokument Kugar документ Кугара Kugar-dokument Kugar belgesi документ Kugar Tài liệu Kugar Kugar 文档 Kugar 文件 KWord document مستند KWord Dakument KWord Документ — KWord document KWord dokument KWord Dogfen KWord KWord-dokument KWord-Dokument Έγγραφο KWord KWord document KWord-dokumento documento de KWord KWord dokumentua KWord-asiakirja KWord skjal document KWord cáipéis KWord documento de KWord מסמך KWord KWord dokument KWord-dokumentum Documento KWord Dokumen KWord Documento KWord KWord ドキュメント KWord құжаты KWord 문서 KWord dokumentas KWord dokuments Dokumen KWord KWord-dokument KWord-document KWord-dokument document KWord Dokument KWord documento KWord Documento do KWord Document KWord документ KWord Dokument KWord Dokument KWord Dokument KWord документ К-речи KWord-dokument KWord belgesi документ KWord Tài liệu KWord KWord 文档 KWord 文件 KWord document (encrypted) مستند KWord (مشفر) Dakument KWord (zašyfravany) Документ — KWord, шифриран document KWord (xifrat) dokument KWord (šifrovaný) KWord-dokument (krypteret) KWord-Dokument (verschlüsselt) Έγγραφο KWord (κρυπτογραφημένο) KWord document (encrypted) KWord-dokumento (ĉifrita) documento de KWord (cifrado) KWord dokumentua (enkriptatua) KWord-asiakirja (salattu) KWord skjal (bronglað) document KWord (chiffré) cáipéis KWord (criptithe) documento de KWord (cifrado) מסמך KWord (מוצפן) KWord dokument (šifriran) KWord-dokumentum (titkosított) Documento KWord (cryptate) Dokumen KWord (terenkripsi) Documento KWord (cifrato) KWord (暗号化) ドキュメント KWord құжаты (шифрленген) 암호화된 KWord 문서 KWord dokumentas (užšifruotas) KWord dokuments (šifrēts) Dokumen Kword (terenkripsi) KWord-dokument (kryptert) KWord-document (versleuteld) Kryptert KWord-dokument document KWord (chifrat) Dokument KWord (zaszyfrowany) documento KWord (encriptado) Documento do KWord (criptografado) Document KWord (criptat) документ KWord (зашифрованный) Dokument KWord (šifrovaný) Dokument KWord (šifriran) Dokument KWord (i kriptuar) документ К-речи (шифровани) KWord-dokument (krypterat) KWord belgesi (şifreli) документ KWord (зашифрований) Tài liệu KWord (đã mật mã) KWord 加密文档 KWord 文件 (已加密) LHA archive أرشيف LHA LHA arxivi Archiŭ LHA Архив — LHA arxiu LHA archiv LHA Archif LHA LHA-arkiv LHA-Archiv Συμπιεσμένο αρχείο LHA LHA archive LHA-arkivo archivador LHA LHA artxiboa LHA-arkisto LHA skjalasavn archive LHA cartlann LHA arquivo LHA ארכיון LHA LHA arhiva LHA-archívum Archivo LHA Arsip LHA Archivio LHA LHA アーカイブ LHA архиві LHA 압축 파일 LHA archyvas LHA arhīvs Arkib LHA LHA-arkiv LHA-archief LHA-arkiv archiu LHA Archiwum LHA arquivo LHA Pacote LHA Arhivă LHA архив LHA Archív LHA Datoteka arhiva LHA Arkiv LHA ЛХА архива LHA-arkiv LHA arşivi архів LHA Kho nén LHA LHA 归档文件 LHA 封存檔 LHZ archive أرشيف LHZ Archiŭ LHZ Архив — LHZ arxiu LHZ archiv LHZ LHZ-arkiv LHZ-Archiv Συμπιεσμένο αρχείο LHZ LHZ archive LHZ-arkivo archivador LHZ LHZ artxiboa LHZ-arkisto LHZ skjalasavn archive LHZ cartlann LHZ arquivo LHZ ארכיון LHZ LHZ arhiva LHZ-archívum Archivo LHZ Arsip LHZ Archivio LHZ LHZ アーカイブ LHZ архиві LHZ 압축 파일 LHZ archyvas LHZ arhīvs Arkib LHZ LHZ-arkiv LHZ-archief LHZ-arkiv archiu LHZ Archiwum LHZ arquivo LHZ Pacote LHZ Arhivă LHZ архив LHZ Archív LHZ Datoteka arhiva LHZ Arkiv LHZ ЛХЗ архива LHZ-arkiv LHZ arşivi архів LHZ Kho nén LHZ (LHA đã nén) LHZ 归档文件 LHZ 封存檔 message catalog كتالوج الرسالة kataloh paviedamleńniaŭ Каталог със съобщения catàleg de missatges katalog zpráv meddelelseskatalog Nachrichtenkatalog Κατάλογος μηνυμάτων message catalogue katalogo de mesaĝoj catálogo de mensajes mezuen katalogoa viestiluettelo boðskrá catalogue de messages catalóg theachtaireachtaí catálogo de mensaxes קטלוג הודעות katalog poruka üzenetkatalógus Catalogo de messages katalog pesan Catalogo di messaggi メッセージカタログ мәлімдемелер каталогы 메시지 카탈로그 laiškų katalogas ziņojumu katalogs Katalog mesej meldingskatalog berichtencatalogus meldingskatalog catalòg de messatges Katalog wiadomości catálogo de mensagens Catálogo de mensagens catalog de mesaje каталог сообщений Katalóg správ katalogov sporočil Katallog mesazhesh каталог порука meddelandekatalog ileti kataloğu каталог повідомлень phân loại thông điệp 消息库 訊息目錄 LyX document مستند LyX Dakument LyX Документ — LyX document LyX dokument LyX LyX-dokument LyX-Dokument Έγγραφο LyX LyX document LyX-dokumento documento de LyX LyX dokumentua LyX-asiakirja LyX skjal document LyX cáipéis LyX documento LyX מסמך Lyx LyX dokument LyX-dokumentum Documento LyX Dokumen LyX Documento LyX LyX ドキュメント LyX құжаты LyX 문서 LyX dokumentas LyX dokuments Dokumen LyX LyX-dokument LyX-document LyX-dokument document LyX Dokument LyX documento LyX Documento LyX Document LyX документ LyX Dokument LyX Dokument LyX Dokument LyX ЛиКс документ LyX-dokument LyX belgesi документ LyX Tài liệu LyX LyX 文档 LyX 文件 LZ4 archive arxiu LZ4 archiv LZ4 LZ4-arkiv LZ4-Archiv Συμπιεσμένο αρχείο LZ4 LZ4 archive archivador LZ4 LZ4 artxiboa LZ4-arkisto archive LZ4 Arquivo LZ4 ארכיון LZ4 LZ4 arhiva LZ4 archívum Archivo LZ4 Arsip LZ4 Archivio LZ4 LZ4 архиві LZ4 압축 파일 archiu LZ4 Archiwum LZ4 arquivo LZ4 Pacote LZ4 архив LZ4 Archív LZ4 Datoteka arhiva LZ4 ЛЗ4 архива LZ4-arkiv LZ4 arşivi архів LZ4 LZ4 归档文件 LZ4 封存檔 Tar archive (LZ4-compressed) Lzip archive أرشيف Lzip Архив — lzip arxiu lzip archiv Lzip Lzip-arkiv Lzip-Archiv Συμπιεσμένο αρχείο Lzip Lzip archive Lzip-arkivo archivador Lzip Lzip artxiboa Lzip-arkisto Lzip skjalasavn archive lzip cartlann Lzip arquivo Lzip ארכיון Lzip Lzip arhiva Lzip archívum Archivo Lzip Arsip Lzip Archivio Lzip Lzip アーカイブ Lzip архиві LZIP 압축 파일 Lzip archyvas Lzip arhīvs Lzip archief archiu lzip Archiwum lzip arquivo LZip Pacote Lzip Arhivă Lzip архив LZIP Archív Lzip Datoteka arhiva Lzip Лзип архива Lzip-arkiv Lzip arşivi архів lzip Lzip 归档文件 Lzip 封存檔 Tar archive (lzip-compressed) LZMA archive أرشيف LZMA Archiŭ LZMA Архив — LZMA arxiu LZMA archiv LZMA LZHA-arkiv LZMA-Archiv Συμπιεσμένο αρχείο LZMA LZMA archive LZMA-arkivo archivador LZMA LZMA artxiboa LZMA-arkisto LZMA skjalasavn archive LZMA cartlann LZMA arquivo LZMA ארכיון LZMA LZMA arhiva LZMA-archívum Archivo LZMA Arsip LZMA Archivio LZMA LZMA アーカイブ LZMA архиві LZMA 압축 파일 LZMA archyvas LZMA arhīvs LZMA-arkiv LZMA-archief LZMA-arkiv archiu LZMA Archiwum LZMA arquivo LZMA Pacote LZMA Arhivă LZMA архив LZMA Archív LZMA Datoteka arhiva LZMA Arkiv LZMA ЛЗМА архива LZMA-arkiv LZMA arşivi архів LZMA Kho nén LZMA LZMA 归档文件 LZMA 封存檔 LZMA Lempel-Ziv-Markov chain-Algorithm Tar archive (LZMA-compressed) أرشيف Tar (مضغوط-LZMA) Archiŭ tar (LZMA-skampresavany) Архив — tar, компресиран с LZMA arxiu tar (amb compressió LZMA) archiv tar (komprimovaný pomocí LZMA) Tar-arkiv (LZMA-komprimeret) Tar-Archiv (LZMA-komprimiert) Αρχείο Tar (συμπιεσμένο με LZMA) Tar archive (LZMA-compressed) archivador Tar (comprimido con LZMA) Tar artxiboa (LZMA-rekin konprimitua) Tar-arkisto (LZMA-pakattu) Tar skjalasavn (LZMA-stappað) archive tar (compression LZMA) cartlann Tar (comhbhrúite le LZMA) arquivo Tar (comprimido con LZMA) ארכיון Tar (מכווץ ע״י LZMA) Tar arhiva (komprimirana LZMA-om) Tar archívum (LZMA-val tömörítve) Archivo Tar (comprimite con LZMA) Arsip Tar (terkompresi LZMA) Archivio tar (compresso con LZMA) Tar アーカイブ (LZMA 圧縮) Tar архиві (LZMA-мен сығылған) TAR 묶음 파일(LZMA 압축) Tar archyvas (suglaudintas su LZMA) Tar arhīvs (saspiests ar LZMA) Tar-arkiv (LZMA-komprimert) Tar-archief (ingepakt met LZMA) Tar-arkiv (pakka med LZMA) archiu tar (compression LZMA) Archiwum tar (kompresja LZMA) arquivo Tar (compressão LZMA) Pacote Tar (compactado com LZMA) Arhivă Tar (comprimată LZMA) архив TAR (сжатый LZMA) Archív tar (komprimovaný pomocou LZMA) Datoteka arhiva Tar (stisnjen z LZMA) Arkiv tar (i kompresuar me LZMA) Тар архива (запакована ЛЗМА-ом) Tar-arkiv (LZMA-komprimerat) Tar arşivi (LZMA ile sıkıştırılmış) архів tar (стиснений LZMA) Kho nén tar (đã nén LZMA) Tar 归档文件 (LZMA 压缩) Tar 封存檔 (LZMA 格式壓縮) LZO archive أرشيف LZO Archiŭ LZO Архив — LZO arxiu LZO archiv LZO LZO-arkiv LZO-Archiv Συμπιεσμένο αρχείο LZO LZO archive LZO-arkivo archivador LZO LZO artxiboa LZO-arkisto LZO skjalasavn archive LZO cartlann LZO arquivo LZO ארכיון LZO LZO arhiva LZO-archívum Archivo LZO Arsip LZO Archivio LZO LZO アーカイブ LZO архиві LZO 압축 파일 LZO archyvas LZO arhīvs Arkib LZO LZO-arkiv LZO-archief LZO-arkiv archiu LZO Archiwum LZO arquivo LZO Pacote LZO Arhivă LZO архив LZO Archív LZO Datoteka arhiva LZO Arkiv LZO ЛЗО архива LZO-arkiv LZO arşivi архів LZO Kho nén LZO LZO 归档文件 LZO 封存檔 LZO Lempel-Ziv-Oberhumer Qpress archive arxiu Qpress archiv Qpress Qpress-arkiv Qpress-Archiv Συμπιεσμένο αρχείο Qpress Qpress archive archivador de Qpress Qpress artxiboa Qpress-arkisto Archive Qpress Arquivo Qpress ארכיון Qpress Qpress arhiva Qpress archívum Archivo Qpress Arsip Qpress Archivio Qpress Qpress архиві Qpress 압축 파일 Archiu Qpress Archiwum Qpress arquivo Qpress Pacote Qpress архив Qpress Archív Qpress Datoteka arhiva Qpress Купрес архива Qpress-arkiv Qpress arşivi архів Qpress Qpress 归档文件 Qpress 封存檔 XAR archive arxiu XAR archiv XAR XAR-Archiv XAR archive archivador XAR XAR artxiboa XAR-arkisto XAR arhiva XAR archívum Arsip XAR Archivio XAR XAR 아카이브 Archiu XAR Archiwum XAR Arquivo XAR архив XAR Archív XAR ИксАР архива XAR-arkiv XAR arşivi архів XAR XAR 归档文件 XAR 封存檔 XAR eXtensible ARchive Zlib archive arxiu Zlib archiv Zlib Zlib-arkiv Zlib-Archiv Συμπιεσμένο αρχείο Zlib Zlib archive archivador Zlib Zlib artxiboa Zlib-arkisto Archive Zlib Arquivo Zlib ארכיון Zlib Zlib arhiva Zlib archívum Archivo Zlib Arsip Zlib Archivio zlib Zlib архиві Zlib 압축 파일 Archiu Zlib Archiwum Zlib arquivo Zlib Pacote Zlib архив Zlib Archív Zlib Datoteka arhiva Zlib Злиб архива Zlib-arkiv Zlib arşivi архів zlib Alzip 归档文件 Zlib 封存檔 MagicPoint presentation عرض تقديمي MagicPoint Prezentacyja MagicPoint Презентация — MagicPoint presentació de MagicPoint prezentace MagicPoint Cyflwyniad MagicPoint MagicPoint-præsentation MagicPoint-Präsentation Παρουσίαση MagicPoint MagicPoint presentation MagicPoint-prezentaĵo presentación de MagicPoint MagicPoint aurkezpena MagicPoint-esitys MagicPoint framløga présentation MagicPoint láithreoireacht MagicPoint presentación de MagicPoint מצגת MagicPoint MagicPoint prezentacija MagicPoint-bemutató Presentation MagicPoint Presentasi MagicPoint Presentazione MagicPoint MagicPoint プレゼンテーション MagicPoint-ის პრეზენტაცია MagicPoint презентациясы MagicPoint 프레젠테이션 MagicPoint pateiktis MagicPoint prezentācija Persembahan MagicPoint MagicPoint-presentasjon MagicPoint-presentatie MagicPoint-presentasjon presentacion MagicPoint Prezentacja programu MagicPoint apresentação MagicPoint Apresentação do MagicPoint Prezentare MagicPoint презентация MagicPoint Prezentácia MagicPoint Predstavitev MagicPoint Prezantim MagicPoint презентација Меџик Поинта MagicPoint-presentation MagicPoint sunumu презентація MagicPoint Trình diễn MagicPoint MagicPoint 演示文稿 MagicPoint 簡報檔 Macintosh MacBinary file ملف Macintosh MacBinary Fajł Macintosh MacBinary Файл — MacBinary fitxer MacBinary de Macintosh soubor MacBinary pro Macintosh Macintosh MacBinary-fil Macintosh-MacBinary-Datei Εκτελέσιμο Macintosh MacBinary Macintosh MacBinary file MacBinary-dosiero de Macintosh archivo de Macintosh MacBinary Macintosh MacBinary fitxategia Macintosh MacBinary -tiedosto Macintosh MacBinary fíla fichier Macintosh MacBinary comhad Macintosh MacBinary ficheiro MacBinary de Macintosh קובץ בינרי של מקינטוש Macintosh MacBinary datoteka Macintosh MacBinary-fájl File MacBinary de Macintosh Berkas Macintosh MacBinary File Macintosh MacBinary Macintosh MacBinary ファイル Macintosh MacBinary файлы MacBinary 파일 Macintosh MacBinary failas Macintosh MacBinary datne Fail MacBinary Macintosh Macintosh MacBinary-fil Macintosh MacBinary-bestand Macintosh MacBinary-fil fichièr Macintosh MacBinary Plik MacBinary Macintosh ficheiro MacBinary de Macintosh Arquivo do Macintosh MacBinary Fișier Macintosh MacBinary файл Macintosh MacBinary Súbor pre Macintosh MacBinary Izvedljiva dvojiška datoteka Macintosh MacBinary File MacBinary Macintosh Мекинтош Мек Бинари датотека Macintosh MacBinary-fil Macintosh MacBinary dosyası файл Macintosh MacBinary Tập tin nhị phân MacBinary của Macintosh Macintosh MacBinary 文件 Macintosh MacBinary 檔 Matroska stream دفق Matroska Płyń Matroska Поток — Matroska flux Matroska proud Matroska Matroskastrøm Matroska-Datenstrom Ροή Matroska Matroska stream flujo Matroska Matroska korrontea Matroska-virta Matroska streymur flux Matroska sruth Matroska fluxo de Matroska זרימת Matroska Matroška zapis Matroska adatfolyam Fluxo Matroska Stream Matroska Stream Matroska Matroska ストリーム Matroska-ის ნაკადი Matroska ағымы Matroska 스트림 Matroska srautas Matroska straume Matroska-stream Matroska-straum flux Matroska Strumień Matroska fluxo Matroska Transmissão do Matroska Flux Matroska поток Matroska Stream Matroska Pretočni vir Matroska Stream Matroska Матрошкин ток Matroska-ström Matroska akışı потік даних Matroska Luồng Matroska Matroska 流 Matroska 串流 Matroska video Matroska مرئي Videa Matroska Видео — Matroska vídeo Matroska video Matroska Matroskavideo Matroska-Video Βίντεο Matroska Matroska video Matroska-video vídeo Matroska Matroska bideoa Matroska-video Matroska video vidéo Matroska físeán Matroska vídeo de Matroska וידאו Matroska Matroska video Matroska-videó Video Matroska Video Matroska Video Matroska Matroska 動画 Matroska-ის ვიდეო Matroska видеосы Matroska 동영상 Matroska vaizdo įrašas Matroska video Video Matroska Matroska-film Matroska-video Matroska-video vidèo Matroska Plik wideo Matroska vídeo Matroska Vídeo Matroska Video Matroska видео Matroska Video Matroska Video datoteka Matroska Video Matroska Матрошкин видео Matroska-video Matroska video відеокліп Matroska Ảnh động Matroska Matroska 视频 Matroska 視訊 Matroska 3D video vídeo Matroska 3D 3D video Matroska Matroska 3D-video Matroska 3D-Video Βίντεο 3Δ Matroska Matroska 3D video vídeo Matroska en 3D Matroska 3D bideoa vidéo Matroska 3D Video Matroska 3D סרטון תלת ממדי מסוג Matroska Matroška 3D video snimka Matroska 3D videó Video 3D Matroska Video 3D Matroska Video Matroska 3D Matroska 3D видеосы Matroska 3D 동영상 vidèo Matroska 3D Plik wideo Matroska 3D vídeo 3D Matroska Vídeo 3D Matroska Видео Matroska 3D 3D video Matroska Video datoteka Matroska 3D Матрошкин 3Д видео Matroska 3D-video Matroska 3B video відеокліп Matroska 3D Matroska 3D 视频 Matroska 3D 視訊 Matroska audio سمعي Matroska Aŭdyjo Matroska Аудио — Matroska àudio de Matroska zvuk Matroska Matroskalyd Matroska-Audio Ήχος Matroska Matroska audio Matroska-sondosiero sonido Matroska Matroska audioa Matroska-ääni Matroska ljóður audio Matroska fuaim Matroska son de Matroska שמע Matroska Matroska audio Matroska hang Audio Matroska Audio Matroska Audio Matroska Matroska オーディオ Matroska-ის აუდიო Matroska аудиосы Matroska 오디오 Matroska garso įrašas Matroska audio Matroska-lyd Matroska-audio Matroska-lyd àudio Matroska Plik dźwiękowy Matroska áudio Matroska Áudio Matroska Audio Matroska аудио Matroska Zvuk Matroska Zvočna datoteka Matroska Audio Matroska Матрошкин звук Matroska-ljud Matroska ses звук Matroska Âm thanh Matroska Matroska 音频 Matroska 音訊 WebM video WebM مرئي Видео — WebM vídeo WebM video WebM WebM-video WebM-Video Βίντεο WebM WebM video WebM-video vídeo WebM WebM bideoa WebM-video WebM video vidéo WebM físeán WebM vídeo WebM וידאו WebM WebM video WebM videó Video WebM Video WebM Video WebM WebM 動画 WebM видеосы WebM 동영상 WebM vaizdo įrašas WebM video WebM video vidèo WebM Plik wideo WebM vídeo WebM Vídeo WebM Video WebM видео WebM Video WebM Video datoteka WebM ВебМ видео WebM-video WebM video відео WebM WebM 视频 WebM 視訊 WebM audio WebM سمعي Аудио — WebM àudio de WebM zvuk WebM WebM-lyd WebM-Audio Ήχος WebM WebM audio WebM-sondosiero sonido WebM WebM audioa WebM-ääni WebM ljóður audio WebM fuaim WebM son WebM שמע WebM WebM audio WebM hang Audio WebM Audio WebM Audio WebM WebM オーディオ WebM аудиосы WebM 오디오 WebM garso įrašas WebM audio WebM audio àudio WebM Plik dźwiękowy WebM áudio WebM Áudio WebM Audio WebM аудио WebM Zvuk WebM Zvočna datoteka WebM ВебМ звук WebM-ljud WebM sesi звук WebM WebM 音频 WebM 音訊 MHTML web archive arxiu web MHTML webový archiv MHTML MHTML-netarkiv MHTML-Webarchiv Συμπιεσμένο αρχείο ιστού MHTML MHTML web archive archivador web MHTML MHTML web artxiboa MHTML-kooste archive web MHTML Arquivo web MHTML ארכיון רשת MHTML MHTML web arhiva MHTML webarchívum Archivo web MHTML Arsip web MHTML Archivio web MHTML MHTML Web アーカイブ MHTML веб архиві MHTML 웹 보관 파일 MHTML tīmekļa arhīvs archiu web MHTML Archiwum witryny MHTML arquivo web MHTML Pacote web MHTML веб-архив MHTML Webový archív MHTML Spletni arhiv MHTML МХТМЛ веб архива MHTML-webbarkiv MHTML web arşivi вебархів MHTML MHTML 网络归档 MHTML 網頁封存檔 MHTML MIME HTML MXF video MXF مرئي Видео — MXF vídeo MXF video MXF MXF-video MXF-Video Βίντεο MXF MXF video MXF-video vídeo MXF MXF bideoa MXF-video MXF video vidéo MXF físeán MXF vídeo MXF וידאו MXF MXF video MXF videó Video MXF Video MXF Video MXF MXF 動画 MXF ვიდეო MXF видеосы MXF 동영상 MXF vaizdo įrašas MXF video MXF video vidèo MXF Plik wideo MXF vídeo MXF Vídeo MXF Video MXF видео MXF Video MXF Video datoteka MXF МИксФ видео MXF-video MXF video відеокліп MXF MXF 视频 MXF 視訊 MXF Material Exchange Format OCL file ملف OCL Fajł OCL Файл — OCL fitxer OCL soubor OCL OCL-fil OCL-Datei Αρχείο OCL OCL file OCL-dosiero archivo OCL OCL fitxategia OCL-tiedosto OCL fíla fichier OCL comhad OCL ficheiro OCL קובץ OCL OCL datoteka OCL fájl File OCL Berkas OCL File OCL OCL ファイル OCL файлы OCL 파일 OCL failas OCL datne OCL-fil OCL-bestand OCL-fil fichièr OCL Plik OCL ficheiro OCL Arquivo OCL Fișier OCL файл OCL Súbor OCL Datoteka OCL File OCL ОЦЛ датотека OCL-fil OCL dosyası файл OCL Tập tin OCL OCL 文件 OCL 檔 OCL Object Constraint Language COBOL source file Изходен код — COBOL codi font en COBOL zdrojový soubor COBOL COBOL-kildefil COBOL-Quelldatei Πηγαίο αρχείο COBOL COBOL source file COBOL-fontdosiero archivo fuente de COBOL COBOL iturburu-kodea COBOL-lähdekoodi fichier source COBOL ficheiro fonte de COBOL קובץ מקור של COBOL COBOL izvorna datoteka COBOL forrásfájl File de codice fonte COBOL Berkas sumber COBOL File sorgente COBOL COBOL ソースファイル COBOL-ის საწყისი ფაილი COBOL бастапқы коды COBOL 소스 파일 COBOL pirmkods COBOL bronbestand fichièr font COBOL Plik źródłowy COBOL ficheiro origem COBOL Arquivo de código-fonte em COBOL файл исходного кода на COBOL Zdrojový súbor COBOLu Izvorna koda COBOL изворна датотека КОБОЛ-а COBOL-källkodsfil COBOL kaynak dosyası вихідний код мовою COBOL COBOL 源 COBOL 源檔 COBOL COmmon Business Oriented Language Mobipocket e-book Е-книга — Mobipocket llibre electrònic Mobipocket elektronická kniha Mobipocket Mobipocket e-bog Mobipocket E-Book Ηλεκτρονικό βιβλίο Mobipocket Mobipocket e-book libro electrónico de Mobipocket Mobipocket liburua Mobipocket e-kirja livre numérique Mobipocket E-book Mobipocket ספר אלקטרוני של Mobipocket Mobipocket e-knjiga Mobipocket e-könyv E-libro Mobipocket e-book Mobipocket E-book Mobipocket Mobipocket 電子書籍 Mobipocket-ის ელწიგნი Mobipocket эл. кітабы Mobipocket 전자책 Mobipocket e-grāmata Mobipocket e-book libre numeric Mobipocket E-book Mobipocket ebook Mobipocket E-book Mobipocket электронная книга Mobipocket E-kniha Mobipocket e-knjiga Mobipocket Мобипокет ел. књига Mobipocket-e-bok Mobipocket e-kitap електронна книга Mobipocket Mobipocket 电子书 Mobipocket e-book Adobe FrameMaker MIF document مستند أدوبي الصانع للإطارات MIF Dakument Adobe FrameMaker MIF Документ — Adobe FrameMaker MIF document MIF d'Adobe FrameMaker dokument Adobe FrameMaker MIF Adobe FrameMaker MIF-dokument Adobe-FrameMaker-MIF-Dokument Έγγραφο MIF του Adobe FrameMaker Adobe FrameMaker MIF document MIF-dokumento de Adobe FrameMaker documento MIF de Adobe FrameMaker Adobe FrameMaker-en MIF dokumentua Adobe FrameMaker MIF -asiakirja Adobe FrameMaker MIF skjal document MIF Adobe FrameMaker cáipéis MIF Adobe FrameMaker documento MIF de Adobe FrameMaker מסמך MIF של Adobe FrameMaker Adobe FrameMaker MIF dokument Adobe FrameMaker MIF-dokumentum Documento MIF de Adobe FrameMaker Dokumen Adobe FrameMaker MIF Documento MIF Adobe FrameMaker Adobe FrameMaker MIF ドキュメント Adobe FrameMaker-ის MIF დოკუმენტი Adobe FrameMaker MIF құжаты Adobe 프레임메이커 MIF 문서 Adobe FrameMaker MIF dokumentas Adobe FrameMaker MIF dokuments Adobe FrameMaker MIF-dokument Adobe FrameMaker MIF-document Adobe FrameMaker MIF-dokument document MIF Adobe FrameMaker Dokument MIF Adobe FrameMaker documento Adobe FrameMaker MIF Documento MIF do Adobe FrameMaker Document Adobe FrameMaker MIF документ Adobe FrameMaker MIF Dokument Adobe FrameMaker MIF Dokument Adobe FrameMaker MIF Dokument MIF Adobe FrameMaker Адобе Фрејм Мејкер МИФ документ Adobe FrameMaker MIF-dokument Adobe FrameMaker MIF belgesi документ Adobe FrameMaker MIF Tài liệu Adobe FrameMaker MIF Adobe FrameMaker MIF 文档 Adobe FrameMaker MIF 文件 Mozilla bookmarks علامات موزيلا Zakładki Mozilla Отметки — Mozilla llista d'adreces d'interès de Mozilla záložky Mozilla Mozillabogmærker Mozilla-Lesezeichen Σελιδοδείκτες Mozilla Mozilla bookmarks Mozilla-legosignoj marcadores de Mozilla Mozillako laster-markak Mozilla-kirjanmerkit Mozilla bókamerki marque-pages Mozilla leabharmharcanna Mozilla Marcadores de Mozilla סימניה של Mozilla Mozilla knjižne oznake Mozilla-könyvjelzők Marcapaginas Mozilla Bookmark Mozilla Segnalibri Mozilla Mozilla ブックマーク Mozilla бетбелгілері 모질라 책갈피 Mozilla žymelės Mozilla grāmatzīmes Tandabuku Mozilla Mozilla-bokmerker Mozilla-bladwijzers Mozilla-bokmerker marcapaginas Mozilla Zakładki Mozilla marcadores do Mozilla Favoritos do Mozilla Semne de carte Mozilla закладки Mozilla Záložky Mozilla Datoteka zaznamkov Mozilla Libërshënues Mozilla Мозилини обележивачи Mozilla-bokmärken Mozilla yer imleri закладки Mozilla Liên kết đã lưu Mozilla Mozilla 书签 Mozilla 書籤 DOS/Windows executable تنفيذي DOS/Windows Vykonvalny fajł DOS/Windows Изпълним файл — DOS/Windows executable de DOS o de Windows spustitelný soubor pro DOS/Windows DOS-/Windowskørbar DOS/Windows-Programmdatei Εκτελέσιμο DOS/Windows DOS/Windows executable DOS/Windows-plenumebla ejecutable de DOS/Windows DOS/Windows-eko exekutagarria DOS/Windows-ohjelma DOS/Windows inningarfør exécutable DOS/Windows comhad inrite DOS/Windows executábel de DOS/Windows קובץ בר־הרצה של DOS/חלונות DOS/Windows izvršna datoteka DOS/Windows futtatható Executabile DOS/Windows DOS/Windows dapat dieksekusi Eseguibile DOS/Windows DOS/Windows 実行ファイル DOS/Windows გაშვებადი ფაილი DOS/Windows орындалатын файлы DOS/Windows 실행 파일 DOS/Windows vykdomasis failas DOS/Windows izpildāmais Bolehlaksana DOS/Windows Kjørbar fil for DOS/Windows DOS/Windows-uitvoerbaar bestand DOS/Windows køyrbar fil executable DOS/Windows Program DOS/Windows executável DOS/Windows Executável do DOS/Windows Executabil DOS/Windows исполняемый файл DOS/Windows Spustiteľný súbor pre DOS/Windows Izvedljiva datoteka DOS/Windows I ekzekutueshëm DOS/Windows ДОС/Виндоуз извршна Körbar DOS/Windows-fil DOS/Windows çalıştırılabilir виконуваний файл DOS/Windows Tập tin có thực hiện được DOS/Windows DOS/Windows 可执行文件 DOS/Windows 可執行檔 Internet shortcut اختصار الإنترنت Sieciŭnaja spasyłka Адрес в Интернет drecera d'Internet odkaz do Internetu Internetgenvej Internet-Verweis Συντόμευση διαδικτύου Internet shortcut acceso directo a Internet Interneteko lasterbidea Internet-pikakuvake Alnetssnarvegur raccourci Internet aicearra Idirlín atallo de Internet קיצור דרך של האינטרנט Internetski prečac Internetes indítóikon Ligamine Internet Jalan pintas Internet Scorciatoia Internet インターネットショートカット Интернет сілтемесі 인터넷 바로 가기 Interneto nuoroda Interneta īsceļš Internettsnarvei internetkoppeling Internett-snarveg acorchi Internet Skrót internetowy atalho da Internet Atalho da Internet Scurtătură Internet Интернет-ссылка Internetový odkaz Internetna bližnjica Shkurtim internet Интернет пречица Internetgenväg İnternet kısayolu інтернет-посилання Lối tắt Internet Internet 快捷方式 網際網路捷徑 WRI document مستند WRI Dakument WRI Документ — WRI document WRI dokument WRI WRI-dokument WRI-Dokument Έγγραφο WRI WRI document WRI-dokumento documento WRI WRI dokumentua WRI-asiakirja WRI skjal document WRI cáipéis WRI documento WRI מסמך WRI WRI dokument WRI dokumentum Documento WRI Dokumen WRI Documento WRI WRI ドキュメント WRI құжаты WRI 문서 WRI dokumentas WRI dokuments WRI-dokument WRI-document WRI-dokument document WRI Dokument WRI documento WRI Documento WRI Document WRI документ WRI Dokument WRI Dokument WRI Dokument WRI ВРИ документ WRI-dokument WRI belgesi документ WRI Tài liệu WRI WRI 文档 WRI 文件 MSX ROM MSX ROM MSX ROM ROM — MSX ROM de MSX ROM pro MSX ROM MSX MSX-rom MSX-ROM MSX ROM MSX ROM MSX-NLM ROM de MSX MSX-ko ROMa MSX-ROM MSX ROM ROM MSX ROM MSX ROM de MSX MSX ROM MSX ROM MSX ROM ROM pro MSX Memori baca-saja MSX ROM MSX MSX ROM MSX-ის ROM MSX ROM MSX 롬 MSX ROM MSX ROM ROM MSX MSX-ROM MSX-ROM MSX-ROM ROM MSX Plik ROM konsoli MSX ROM MSX ROM do MSX ROM MSX MSX ROM ROM pre MSX Bralni pomnilnik MSX ROM MSX МСИкс РОМ MSX-rom MSX ROM ППП MSX ROM MSX MSX ROM MSX ROM M4 macro M4 macro Makras M4 Макроси — M4 macro M4 makro M4 M4-makro M4-Makro Μακροεντολή m4 M4 macro macro M4 M4 makroa M4-makro M4 fjølvi macro M4 macra M4 macro M4 מאקרו M4 M4 makro M4 makró Macro M4 Makro M4 Macro M4 M4 マクロ M4 макросы M4 매크로 M4 macro M4 makross M4-makro M4-macro M4-makro macro M4 Makro M4 macro M4 Macro M4 Macro M4 макрос M4 Makro M4 Makro datoteka M4 Macro M4 М4 макро M4-makro M4 makrosu макрос M4 Vĩ lệnh M4 M4 宏 M4 巨集 Nintendo64 ROM Nintendo64 ROM Nintendo64 ROM ROM — Nintendo64 ROM de Nintendo64 ROM pro Nintendo64 Nintendo64-rom Nintendo64-ROM Nintendo64 ROM Nintendo64 ROM Nintendo64-NLM ROM de Nintendo64 Nintendo64-ko ROMa Nintendo64-ROM Nintendo64 ROM ROM Nintendo64 ROM Nintendo64 ROM de Nintendo64 ROM של Nintendo64 Nintendo64 ROM Nintendo64 ROM ROM pro Nintendo64 Memori baca-saja Nintendo64 ROM Nintendo64 Nintendo64 ROM Nintendo64 ROM 닌텐도 64 롬 Nintendo64 ROM Nintendo64 ROM ROM Nintendo64 Nintendo64-ROM Nintendo64-ROM Nintendo64-ROM ROM Nintendo64 Plik ROM konsoli Nintendo64 ROM Nintendo64 ROM do Nintendo64 ROM Nintendo64 Nintendo64 ROM ROM pre Nintendo64 Bralni pomnilnik Nintendo64 ROM Nintendo64 Нинтендо64 РОМ Nintendo64-rom Nintendo64 ROM ППП Nintendo64 ROM Nintendo64 Nintendo64 ROM Nintendo64 ROM Nautilus link وصلة Nautilus Nautilus körpüsü Spasyłka Nautilus Връзка — Nautilus enllaç de Nautilus odkaz Nautilus Cyswllt Nautilus Nautilus-henvisning Nautilus-Verknüpfung Σύνδεσμος Nautilus Nautilus link Nautilus-ligilo enlace de Nautilus Nautilus esteka Nautilus-linkki Nautilus leinkja lien Nautilus nasc Nautilus ligazón de nautilus קישור של Nautilus Nautilus veza Nautilus-link Ligamine Nautilus Taut Nautilus Collegamento Nautilus Nautilus リンク Nautilus сілтемесі 노틸러스 바로 가기 Nautilus nuoroda Nautilus saite Pautan Nautilus Nautilus-lenke Nautilus-verwijzing Nautilus-lenke ligam Nautilus Odnośnik Nautilus atalho Nautilus Link do Nautilus Legătură Nautilus ссылка Nautilus Odkaz Nautilus Datoteka povezave Nautilus Lidhje Nautilus Наутилусова веза Nautiluslänk Nautilus bağlantısı посилання Nautilus Liên kết Nautilus Nautilus 链接 Nautilus 鏈結 Neo-Geo Pocket ROM NES ROM NES ROM NES ROM ROM — NES ROM de NES ROM pro NES ROM NES NES-rom NES-ROM NES ROM NES ROM NES-NLM ROM de NES NES-eko ROMa NES-ROM NES ROM ROM NES ROM NES ROM de NES ROM של NES NES ROM NES ROM ROM pro NES Memori baca-saja NES ROM NES ファミコン ROM NES ROM NES 롬 NES ROM NES ROM ROM NES NES ROM Nintendo NES-ROM ROM NES Plik ROM konsoli NES ROM NES ROM do NES ROM NES NES ROM ROM pre NES Bralni pomnilnik NES ROM NES НЕС РОМ NES-rom NES ROM ППП NES ROM NES NES ROM 任天堂 ROM Unidata NetCDF document مستند Unidata NetCDF Dakument Unidata NetCDF Документ — Unidata NetCDF document d'Unidata NetCDF dokument Unidata NetCDF Unidata NetCDF-dokument Unidata-NetCDF-Dokument Έγγραφο Unidata NetCDF Unidata NetCDF document dokumento en NetCDF-formato de Unidata documento de Unidata NetCDF Unidata NetCDF dokumentua Unidata NetCDF -asiakirja Unidata NetCDF skjal document Unidata NetCDF cáipéis Unidata NetCDF Documentno de Unixdata NetCDF מסמך של Unidata NetCDF Unidata NetCDF dokument Unidata NetCDF-dokumentum Documento Unidata NetCDF Dokumen Unidata NetCDF Documento Unidata NetCDF Unidata NetCDF ドキュメント Unidata NetCDF құжаты Unidata NetCDF 문서 Unidata NetCDF dokumentas Unidata NetCDF dokuments Dokumen Unidata NetCDF Unidata NetCDF-dokument Unidata NetCDF-document Unidata netCDF-dokument document Unidata NetCDF Dokument Unidata NetCDF documento Unidata NetCDF Documento do Unidata NetCDF Document Unidata NetCDF документ Unidata NetCDF Dokument Unidata NetCDF Dokument Unidata NetCDF Dokument Unidata NetCDF документ Унидата НетЦДФ-а Unidata NetCDF-dokument Unidata NetCDF belgesi документ Unidata NetCDF Tài liệu NetCDF Unidata Unidata NetCDF 文档 Unidata NetCDF 文件 NetCDF Network Common Data Form NewzBin usenet index Индекс — Usenet, NewzBin índex d'Usenet NewzBin index NewzBin diskuzních skupin Usenet NewzBin-brugernetindex NewzBin-Usenet-Index Ευρετήριο usenet NewzBin NewzBin usenet index índice NewzBin de usenet NewzBin usenet indizea index usenet Índice de usenet NEwzBin אינדקס שרתי חדשות NewzBin NewzBin usenet indeks NewzBin usenet index Indice de usenet NewzBin Indeks usenet NewzBin Indice Usenet NewzBiz NewzBin Usenet インデックス NewzBin usenet индексі NewzBin 유즈넷 인덱스 NewzBin usenet rādītājs NewzBin usenet index indèx usenet NewzBin Indeks grup dyskusyjnych NewzBin índice usenet NewzBin Índice de usenet NewzBin индекс usenet NewzBin Index Usenetu NewzBin Kazalo usenet NewzBin Њузбин попис јузнета NewzBin-usenetindex NewzBin usenet dizini покажчик usenet NewzBin NewzBin usenet 索引 NewzBin usenet 索引 object code رمز الكائن abjektny kod Обектен код codi objecte objektový kód objektkode Objektcode Μεταφρασμένος κώδικας object code celkodo código objeto objektu kodea objektikoodi code objet cód réada código obxecto קוד אובייקט kod objekta tárgykód Codice objecto kode object Codice oggetto オブジェクトコード объектті коды 개체 코드 objektinis kodas objekta kods Kod objek objektkode objectcode objektkode còde objet Kod obiektowy código de objeto Código-objeto cod sursă obiect объектный код Objektový kód predmetna koda Kod objekti објектни ко̂д objektkod nesne kodu об'єктний код mã đối tượng 目标代码 目的碼 Annodex exchange format صيغة Annodex البديلة Формат за обмяна — Annodex format d'intercanvi Annodex výměnný formát Annodex Udvekslingsformat for Annodex Annodex-Wechselformat Μορφή ανταλλαγής Annodex Annodex exchange format formato de intercambio de Annodex Annodex trukatze-formatua Annodex-siirtomuoto Annodex umbýtingarsnið format d'échange Annodex formáid mhalairte Annodex formato intercambiábel de Annodex תבנית החלפת Annodex Annodex oblik za razmjenu Annodex csereformátum Formato de excambio Annodex Format pertukaran Annodex Formato di scambio Annodex Annodex 交換フォーマット Annodex-ის გაცვლითი ფორმატი Annodex алмасу пішімі Annodex 교환 형식 Annodex mainų formatas Annodex apmaiņas formāts Annodex-exchange format d'escambi Annodex Format wymiany Annodex formato de troca Annodex Formato de troca Annodex Format schimb Annodex формат обмена Annodex Formát pre výmenu Annodex Izmenjalna datoteka Annodex Анодексов запис размене Annodex-utväxlingsformat Annodex değişim biçimi формат обміну даними Annodex Định dạng trao đổi Annodex Annodex 交换格式 Annodex 交換格式 Annodex Video Annodex مرئي Видео — Annodex Annodex Video video Annodex Annodexvideo Annodex-Video Βίντεο Annodex Annodex Video Annodex-video vídeo Annodex Annodex bideoa Annodex-video Annodex video vidéo Annodex físeán Annodex vídeo de Annodex וידאו Annodex Annodex Video Annodex videó Video Annodex Video Annodex Video Annodex Annodex 動画 Annodex-ის ვიდეო Annodex видеосы Annodex 동영상 Annodex vaizdo įrašas Annodex video Annodex Video vidèo Annodex Plik wideo Annodex vídeo Annodex Vídeo Annodex Video Annodex видео Annodex Video Annodex Video datoteka Annodex Анодекс видео Annodex-video Annodex Video відеокліп Annodex Ảnh động Annodex Annodex 视频 Annodex 視訊 Annodex Audio Annodex سمعي Аудио — Annodex Annodex Audio zvuk Annodex Annodexlyd Annodex-Audio Ήχος Annodex Annodex Audio Annodex-sondosiero sonido Annodex Annodex audioa Annodex-ääni Annodex ljóður audio Annodex fuaim Annodex son de Annodex שמע Annodex Annodex Audio Annodex hang Audio Annodex Audio Annodex Audio Annodex Annodex オーディオ Annodex-ის აუდიო Annodex аудиосы Annodex 오디오 Annodex garso įrašas Annodex audio Annodex Audio àudio Annodex Plik dźwiękowy Annodex áudio Annodex Áudio Annodex Audio Annodex аудио Annodex Zvuk Annodex Zvočna datoteka Annodex Анодекс аудио Annodex-ljud Annodex Sesi звук Annodex Âm thanh Annodex Annodex 音频 Annodex 音訊 Ogg multimedia file ملف وسائط متعددة Ogg Multymedyjny fajł Ogg Мултимедия — Ogg fitxer multimèdia Ogg multimediální soubor Ogg Ogg multimedie-fil Ogg-Multimediadatei Αρχείο πολυμέσων Ogg Ogg multimedia file archivo multimedia Ogg Ogg multimediako fitxategia Ogg-multimediatiedosto Ogg margmiðlafíla fichier multimédia Ogg comhad ilmheán Ogg ficheiro multimedia Ogg קובץ מולטימדיה Ogg Ogg multimedijalna datoteka Ogg multimédiafájl File multimedial Ogg Berkas multimedia Ogg File multimediale Ogg Ogg マルチメディアファイル Ogg-ის მულტიმედია ფაილი Ogg мультимедиа файлы Ogg 멀티미디어 파일 Ogg multimedijos failas Ogg multimediju datne Ogg-multimediafil Ogg-multimediabestand Ogg multimediafil fichièr multimèdia Ogg Plik multimedialny Ogg ficheiro multimédia Ogg Arquivo multimídia Ogg Fișier multimedia Ogg мультимедийный файл Ogg Súbor multimédií Ogg Večpredstavnostna datoteka Ogg File multimedial Ogg Огг мултимедијална датотека Ogg-multimediafil Ogg çokluortam dosyası мультимедійний файл Ogg Tập tin đa phương tiện Ogg Ogg 多媒体文件 Ogg 多媒體檔案 Ogg Audio Ogg سمعي Aŭdyjo Ogg Аудио — Ogg àudio d'Ogg zvuk Ogg Ogg-lyd Ogg-Audio Ήχος Ogg Ogg Audio sonido Ogg Ogg audioa Ogg-ääni Ogg ljóður audio Ogg fuaim Ogg son Ogg שמע Ogg Ogg zvučni zapis Ogg hang Audio Ogg Audio Ogg Audio Ogg Ogg オーディオ Ogg-ის აუდიო Ogg аудиосы Ogg 오디오 Ogg garso įrašas Ogg audio Ogg lyd Ogg-audio Ogg-lyd àudio Ogg Plik dźwiękowy Ogg áudio Ogg Áudio Ogg Audio Ogg аудио Ogg Zvuk Ogg Zvočna datoteka Ogg Audio Ogg Огг звук Ogg-ljud Ogg Sesi звук ogg Âm thanh Ogg Ogg 音频 Ogg 音訊 Ogg Video Ogg مرئي Videa Ogg Видео — Ogg vídeo Ogg video Ogg Ogg-video Ogg-Video Βίντεο Ogg Ogg Video vídeo Ogg Ogg bideoa Ogg-video Ogg Video vidéo Ogg físeán Ogg vídeo Ogg וידאו Ogg Ogg video snimka Ogg videó Video Ogg Video Ogg Video Ogg Ogg 動画 Ogg ვიდეო Ogg видеосы Ogg 동영상 Ogg vaizdo įrašas Ogg video Ogg video Ogg-video Ogg-video vidèo Ogg Plik wideo Ogg vídeo Ogg Vídeo Ogg Video Ogg видео Ogg Video Ogg Video datoteka Ogg Video Ogg Огг снимак Ogg-video Ogg Video відеокліп ogg Ảnh động Ogg Ogg 视频 Ogg 視訊 Ogg Vorbis audio Ogg Vorbis سمعي Ogg Vorbis audio faylı Aŭdyjo Ogg Vorbis Аудио — Ogg Vorbis àudio d'Ogg Vorbis zvuk Ogg Vorbis Sain Ogg Vorbis Ogg Vorbis-lyd Ogg-Vorbis-Audio Ήχος Ogg Vobris Ogg Vorbis audio Ogg-Vorbis-sondosiero sonido Ogg Vorbis Ogg Vorbis audioa Ogg Vorbis -ääni Ogg Vorbis ljóður audio Ogg Vorbis fuaim Ogg Vorbis son Ogg Vorbis שמע Ogg Vorbis Ogg Vorbis zvučni zapis Ogg Vorbis hang Audio Ogg Vorbis Audio Ogg Vorbis Audio Ogg Vorbis Ogg Vorbis オーディオ Ogg Vorbis აუდიო Ogg Vorbis аудиосы Ogg Vorbis 오디오 Ogg Vorbis garso įrašas Ogg Vorbis audio Audio Ogg Vorbis Ogg Vorbis lyd Ogg Vorbis-audio Ogg Vorbis-lyd àudio Ogg Vorbis Plik dźwiękowy Ogg Vorbis áudio Ogg Vorbis Áudio Ogg Vorbis Audio Ogg Vorbis аудио Ogg Vorbis Zvuk Ogg Vorbis Zvočna datoteka Ogg Vorbis Audio Ogg Vorbis Огг Ворбис звук Ogg Vorbis-ljud Ogg Vorbis sesi звук ogg Vorbis Âm thanh Vorbis Ogg Ogg Vorbis 音频 Ogg Vorbis 音訊 Ogg FLAC audio Ogg FLAC سمعي Aŭdyjo Ogg FLAC Аудио — Ogg FLAC àudio FLAC d'Ogg zvuk Ogg FLAC Ogg FLAC-lyd Ogg-FLAC-Audio Ήχος Ogg FLAC Ogg FLAC audio sonido Ogg FLAC Ogg FLAC audioa Ogg FLAC -ääni Ogg FLAC ljóður audio Ogg FLAC fuaim Ogg FLAC son Ogg FLAC שמע Ogg FLAC Ogg FLAC zvučni zapis Ogg FLAC hang Audio Ogg FLAC Audio Ogg FLAC Audio Ogg FLAC Ogg FLAC オーディオ Ogg FLAC აუდიო Ogg FLAC аудиосы Ogg FLAC 오디오 Ogg FLAC garso įrašas Ogg FLAC audio Ogg FLAC-lyd Ogg FLAC-audio Ogg FLAC-lyd àudio Ogg FLAC Plik dźwiękowy Ogg FLAC áudio Ogg FLAC Áudio Ogg FLAC Audio Ogg FLAC аудио Ogg FLAC Zvuk Ogg FLAC Zvočna datoteka Ogg FLAC Audio Ogg FLAC Огг ФЛАЦ звук Ogg FLAC-ljud Ogg FLAC sesi звук ogg FLAC Âm thanh FLAC Ogg Ogg FLAC 音频 Ogg FLAC 音訊 Opus audio àudio d'Opus zvuk Opus Opus-lyd Opus-Audio Ήχος Opus Opus audio sonido Opus Opus audioa Opus-ääni audio Opus Son Opus שמע Opus Opus zvučni zapis Opus hang Audio Opus Audio Opus Audio Opus Opus аудиосы Opus 오디오 àudio Opus Plik dźwiękowy Opus áudio Opus Áudio Opus Аудио Opus Zvuk Opu Zvočna datoteka Opus Опус звук Opus-ljud Opus sesi звук Opus Opus 音频 Opus 音訊 Ogg Speex audio Ogg Speex سمعي Aŭdyjo Ogg Speex Аудио — Ogg Speex àudio Speex d'Ogg zvuk Ogg Speex Ogg Speex-lyd Ogg-Speex-Audio Ήχος Ogg Speex Ogg Speex audio sonido Ogg Speex Ogg Speex audioa Ogg Speex -ääni Ogg Speex ljóður audio Ogg Speex fuaim Ogg Speex son Ogg Speex שמע Ogg Speex Ogg Speex zvučni zapis Ogg Speex hang Audio Ogg Speex Audio Ogg Speex Audio Ogg Speex Ogg Speex オーディオ Ogg Speex აუდიო Ogg Speex аудиосы Ogg Speex 오디오 Ogg Speex garso įrašas Ogg Speex audio Ogg Speex lyd Ogg Speex-audio Ogg Speex-lyd àudio Ogg Speex Plik dźwiękowy Ogg Speex áudio Ogg Speex Áudio Ogg Speex Audio Ogg Speex аудио Ogg Speex Zvuk Ogg Speex Zvočna datoteka Ogg Speex Audio Ogg Speex Огг Спикс звук Ogg Speex-ljud Ogg Speex sesi звук ogg Speex Âm thanh Speex Ogg Ogg Speex 音频 Ogg Speex 音訊 Speex audio Speex سمعي Aŭdyjo Speex Аудио — Speex àudio de Speex zvuk Speex Speexlyd Speex-Audio Ήχος Speex Speex audio sonido Speex Speex audioa Speex-ääni Speex ljóður audio Speex fuaim Speex son Speex שמע של Speex Speex audio Speex hang Audio Speex Audio Speex Audio Speex Speex オーディオ Speex аудиосы Speex 오디오 Speex garso įrašas Speex audio Speex lyd Speex-audio Speex-lyd àudio Speex Plik dźwiękowy Speex áudio Speex Áudio Speex Audio Speex аудио Speex Zvuk Speex Zvočna datoteka Speex Audio Speex Спикс звук Speex-ljud Speex sesi звук Speex Âm thanh Speex Speex 音频 Speex 音訊 Ogg Theora video Ogg Theora مرئي Videa Ogg Theora Видео — Ogg Theora vídeo Ogg Theora video Ogg Theora Ogg Theora-video Ogg-Theora-Video Βίντεο Ogg Theora Ogg Theora video vídeo Ogg Theora Ogg Theora bideoa Ogg Theora -video Ogg Theora video vidéo Ogg Theora físeán Ogg Theora vídeo Ogg Theora שמע Ogg Theora Ogg Theora video snimka Ogg Theora videó Video Ogg Theora Video Ogg Theora Video Ogg Theora Ogg Theora 動画 Ogg Theora ვიდეო Ogg Theora видеосы Ogg Theora 동영상 Ogg Theora vaizdo įrašas Ogg Theora video Ogg Theora video Ogg Theora-video Ogg Theora-video vidèo Ogg Theora Plik wideo Ogg Theora vídeo Ogg Theora Vídeo Ogg Theora Video Ogg Theora видео Ogg Theora Video Ogg Theora Video datoteka Ogg Theora Video Ogg Theora Огг Теора видео Ogg Theora-video Ogg Theora video відеокліп ogg Theora Ảnh động Theora Ogg Ogg Theora 视频 Ogg Theora 視訊 OGM video OGM مرئي Videa OGM Видео — OGM vídeo OGM video OGM OGM-video OGM-Video Βίντεο OGM OGM video OGM-video vídeo OGM OGM bideoa OGM-video OGM video vidéo OGM físeán OGM vídeo OGM וידאו OGM OGM video OGM-videó Video OGM Video OGM Video OGM OGM 動画 OGM ვიდეო OGM видеосы OGM 동영상 OGM vaizdo įrašas OGM video OGM-film OGM-video OGM-video vidèo OGM Plik wideo OGM vídeo OGM Vídeo OGM Video OGM видео OGM Video OGM Video datoteka OGM Video OGM ОГМ видео OGM-video OGM video відеокліп OGM Ảnh động OGM OGM 视频 OGM 視訊 OLE2 compound document storage تخزين مجمع مستند OLE2 Schovišča dla kampanentaŭ dakumentu OLE2 Съставен документ-хранилище — OLE2 emmagatzematge de documents compostos OLE2 úložiště složeného dokumentu OLE2 OLE2-sammensat dokumentlager OLE2-Verbunddokumentenspeicher Αρχείο συμπαγούς αποθήκευσης εγγράφων OLE2 OLE2 compound document storage OLE2-deponejo de parentezaj dokumentoj almacenamiento de documentos compuestos OLE2 OLE2 konposatutako dokumentu-bilduma OLE2-yhdisteasiakirjatallenne OLE2 samansett skjalagoymsla document de stockage composé OLE2 stóras cháipéisí comhshuite OLE2 almacenamento de documento composto OLE2 אחסון מסמך משותף OLE2 OLE2 pohrana složenog dokumenta OLE2 összetett dokumentumtároló Magazin de documentos composite OLE2 penyimpan dokumen kompon OLE2 Memorizzazione documento composto OLE2 OLE2 複合ドキュメントストレージ OLE2 құрама құжаттар қоймасы OLE2 복합 문서 OLE2 sudėtinių dokumentų laikmena OLE2 savienoto dokumentu glabātuve Storan dokumen halaman OLE2 OLE-lager for sammensatte dokumenter OLE2-samengestelde documentopslag OLE2 lager for samansett dokument document d'emmagazinatge compausat OLE2 Magazyn dokumentu złożonego OLE2 armazenamento de documento composto OLE2 Armazenamento de documento composto OLE2 Document de stocare compus OLE2 хранилище составных документов OLE2 Úložisko zloženého dokumentu OLE2 Združeni dokument OLE2 Arkiv dokumenti i përbërë OLE2 смештај ОЛЕ2 сједињеног документа Sammansatt OLE2-dokumentlager OLE2 bileşik belge depolama сховище складних документів OLE2 Kho lưu tài liệu ghép OLE2 OLE2 组合文档存储 OLE2 複合文件儲存 Microsoft Publisher document document de Microsoft Publisher dokument Microsoft Publisher Microsoft Publisher-dokument Microsoft-Publisher-Dokument Έγγραφο Microsoft Publisher Microsoft Publisher document documento de Microsoft Publisher Microsoft Publisher dokumentua Microsoft Publisher -asiakirja document Microsoft Publisher Documento de Microsoft Publisher מסמך Microsoft Publisher Microsoft Publisher dokument Microsoft Publisher dokumentum Documento Microsoft Publisher Dokumen Microsoft Publisher Documento Microsoft Publisher Microsoft Publisher құжаты Microsoft Publisher 문서 document Microsoft Publisher Dokument Microsoft Publisher documento Microsoft Publisher Documento do Microsoft Publisher Документ Microsoft Publisher Dokument Microsoft Publisher Dokument Microsoft Publisher документ Мајкрософтовог Издавача Microsoft Publisher-dokument Microsoft Publisher belgesi документ Microsoft Publisher Microsoft Publisher 文档 Microsoft Publisher 文件 Windows Installer package حزمة مثبّت ويندوز Pakunak Windows Installer Пакет — инсталация за Windows paquet de Windows Installer balíček Windows Installer Windows Installer-pakke Windows-Installationspaket Πακέτο Windows Installer Windows Installer package paquete de instalación de Windows Windows-eko pakete instalatzailea Windows-asennuspaketti Windows innleggingarpakki paquet d'installation Windows pacáiste Windows Installer paquete de instalación de Windows חבילה של Windows Installer Windows Installer paket Windows Installer csomag Pacchetto Windows Installer Paket Windows Installer Pacchetto Windows Installer Windows インストーラパッケージ Windows Installer дестесі Windows 설치 패키지 Windows Installer paketas Windows Installer pakotne Windows-installatiepakket Windows Installer-pakke paquet d'installacion Windows Pakiet instalatora Windows pacote de instalação Windows Pacote do Windows Installer Pachet instalator Windows пакет Windows Installer Balík Windows Installer Datoteka paketa Windows namestilnika Paketë Windows Installer пакет Виндоузовог инсталатера Windows Installer-paket Windows Installer paketi пакунок Windows Installer Gói cài đặt Windows Windows 程序安装包 Windows Installer 軟體包 GNU Oleo spreadsheet جدول جنو Oleo Raźlikovy arkuš GNU Oleo Таблица — GNU Oleo full de càlcul de GNU Oleo sešit GNU Oleo GNU Oleo-regneark GNU-Oleo-Tabelle Λογιστικό φύλλο GNU Oleo GNU Oleo spreadsheet Kalkultabelo de GNU Oleo hoja de cálculo de GNU Oleo GNU Oleo kalkulu-orria GNU Oleo -taulukko GNU Oleo rokniark feuille de calcul GNU Oleo scarbhileog GNU Oleo folla de cálculo de Oleo GNU גליון נתונים של GNU Oleo GNU Oleo proračunska tablica GNU Oleo-munkafüzet Folio de calculo GNU Oleo Lembar sebar GNU Oleo Foglio di calcolo GNU Oleo GNU Oleo スプレッドシート GNU Oleo ცხრილი GNU Oleo электрондық кестесі GNU Oleo 스프레드시트 GNU Oleo skaičialentė GNU Oleo izklājlapa Hamparan GNU Oleo GNU Oleo regneark GNU Oleo-rekenblad GNU Oleo-rekneark fuèlh de calcul GNU Oleo Arkusz GNU Oleo folha de cálculo GNU Oleo Planilha do GNU Oleo Foaie de calcul GNU Oleo электронная таблица GNU Oleo Zošit GNU Oleo Preglednica GNU Oleo Fletë llogaritje GNU Oleo ГНУ Олео табела GNU Oleo-kalkylblad GNU Oleo çalışma sayfası ел. таблиця GNU Oleo Bảng tính Oleo của GNU GNU Oleo 工作簿 GNU Oleo 試算表 PAK archive أرشيف PAK Archiŭ PAK Архив — PAK arxiu PAK archiv PAK PAK-arkiv PAK-Archiv Συμπιεσμένο αρχείο PAK PAK archive PAK-arkivo archivador PAK PAK artxiboa PAK-arkisto PAK skjalasavn archive PAK cartlann PAK arquivo PAK ארכיון PAK PAK arhiva PAK-archívum Archivo PAK Arsip PAK Archivio PAK PAK アーカイブ PAK არქივი PAK архиві PAK 압축 파일 PAK archyvas PAK arhīvs PAK-arkiv PAK-archief PAK-arkiv archiu PAK Archiwum PAK arquivo PAK Pacote PAK Arhivă PAK архив PAK Archív PAK Datoteka arhiva PAK Arkiv PAK ПАК архива PAK-arkiv PAK arşivi архів PAK Kho nén PAK AR 归档文件 PAK 封存檔 Palm OS database قاعدة بيانات Palm OS Palm OS mə'lumat bazası Baza źviestak Palm OS База от данни — Palm OS base de dades Palm OS databáze Palm OS Cronfa Ddata Palm OS Palm OS-database Palm-OS-Datenbank Βάση δεδομένων Palm OS Palm OS database datumbazo de Palm OS base de datos de Palm OS Palm OS datu-basea Palm OS -tietokanta Palm OS dátustovnur base de données Palm OS bunachar sonraí Palm OS base de datos de Palm OS מסד נתונים של Palm OS Palm OS baza podataka Palm OS-adatbázis Base de datos Palm OS Basis data Palm OS Database Palm OS Palm OS データベース Palm OS дерекқоры Palm OS 데이터베이스 Palm OS duomenų bazė Palm OS datubāze Pangkalandata PalmOS Palm OS-database Palm OS-gegevensbank Palm OS-database banca de donadas Palm OS Baza danych Palm OS base de dados Palm OS Banco de dados do Palm OS Bază de date Palm OS база данных Palm OS Databáza Palm OS Podatkovna zbirka Palm OS Bankë me të dhëna Palm OS база података Палм ОС-а Palm OS-databas Palm OS veritabanı база даних Palm OS Cơ sở dữ liệu PalmOS Palm OS 数据库 Palm OS 資料庫 Parchive archive أرشيف Parchive Archiŭ Parchive Архив — parchive arxiu Parchive archiv Parchive Parchive-arkiv Parchive-Archiv Συμπιεσμένο αρχείο Parchive Parchive archive archivador Parchive Parchive artxiboa Parchive-arkisto Parchive skjalasavn archive Parchive cartlann Parchive arquivo Parchive ארכיון של Parchive Parchive arhiva Parchive archívum Archivo Parchive Arsip Parchive Archivio Parchive Parchive アーカイブ Parchive архиві Parchive 압축 파일 Parchive archyvas Parchive arhīvs Parchive-arkiv Parchive-archief Parchive-arkiv archiu Parchive Archiwum parchive arquivo Parchive Pacote Parchive Arhivă Parchive архив Parchive Archív Parchive Datoteka arhiva Parchive Arkiv Parchive архива Пархива Parchive-arkiv Parchive arşivi архів Parchive Kho nén Parchive Parchive 归档文件 Parchive 封存檔 Parchive Parity Volume Set Archive PEF executable PEF تنفيذي Vykonvalny fajł PEF Изпълним файл — PEF executable PEF spustitelný soubor PEF PEF-kørbar PEF-Programm Εκτελέσιμο PEF PEF executable PEF-plenumebla ejecutable PEF PEF exekutagarria PEF-ohjelma PEF inningarfør exécutable PEF comhad inrite PEF Executábel PEF קובץ הרצה PEF PEF izvršna datoteka PEF futtatható Executabile PEF PEF dapat dieksekusi Eseguibile PEF PEF 実行ファイル PEF орындалатын файлы PEF 실행 파일 PEF vykdomasis failas PEF izpildāmais Bolehlaksana PEF PEF-kjørbar PEF-uitvoerbaar bestand Køyrbar PEF-fil executable PEF Program PEF executável PEF Executável PEF Executabil PEF исполняемый файл PEF Spustiteľný súbor PEF Izvedljiva datoteka PEF E ekzekutueshme PEF ПЕФ извршна Körbar PEF-fil PEF çalıştırılabilir виконуваний файл PEF Tập tin thực hiện được PEF PEF 可执行文件 PEF 可執行檔 Perl script سكربت بيرل Skrypt Perl Скрипт — Perl script Perl skript v Perlu Sgript Perl Perlprogram Perl-Skript Δέσμη ενεργειών Perl Perl script Perl-skripto secuencia de órdenes en Perl Perl script-a Perl-komentotiedosto Perl boðrøð script Perl script Perl Script de Perl תסריט מעטפת של Perl Perl skripta Perl-parancsfájl Script Perl Skrip Perl Script Perl Perl スクリプト Perl сценарийі 펄 스크립트 Perl scenarijus Perl skripts Skrip Perl Perl skript Perl-script Perl-skript escript Perl Skrypt Perl script Perl Script Perl Script Perl сценарий Perl Skript jazyka Perl Skriptna datoteka Perl Script Perl Перл скрипта Perlskript Perl betiği скрипт на Perl Văn lệnh Perl Perl 脚本 Perl 指令稿 PHP script سكربت PHP PHP skripti Skrypt PHP Скрипт — PHP script PHP skript PHP Sgript PHP PHP-program PHP-Skript Δέσμη ενεργειών PHP PHP script PHP-skripto secuencia de órdenes en PHP PHP script-a PHP-komentotiedosto PHP boðrøð script PHP script PHP Script de PHP תסריט מעטפת של PHP PHP skripta PHP-parancsfájl Script PHP Skrip PHP Script PHP PHP スクリプト PHP сценарийі PHP 스크립트 PHP scenarijus PHP skripts Skrip PHP PHP-skript PHP-script PHP-skript escript PHP Skrypt PHP script PHP Script PHP Script PHP сценарий PHP Skript PHP Skriptna datoteka PHP Script PHP ПХП скрипта PHP-skript PHP betiği скрипт PHP Văn lệnh PHP PHP 脚本 PHP 指令稿 PKCS#7 certificate bundle رزمة الشهادة PKCS#7 Сбор със сертификати — PKCS#7 conjunt de certificats PKCS#7 svazek certifikátů PKCS#7 PKCS#7-certifikatbundt PKCS#7-Zertifikatspaket Πακέτο ψηφιακών πιστοποιητικών PKCS#7 PKCS#7 certificate bundle lote de certificados PCKS#7 PKCS#7 zertifikazio sorta PKCS#7-varmennenippu PKCS#7 váttanar bundi lot de certificats PKCS#7 cuach theastas PKCS#7 paquete de certificado PKCS#7 בקשה מוסמכת PKCS#7 PKCS#7 paket vjerodajnica PKCS#7-tanúsítványcsomag Pacchetto de certificatos PKCS#7 Bundel sertifikat PKCS#7 Bundle certificato PKCS#7 PKCS#7 証明書 PKCS#7 сертификаттар дестесі PKCS#7 인증서 묶음 PKCS#7 liudijimų ryšulys PKCS#7 sertifikātu saišķis PKCS#7-certificaatbundel lòt de certificats PKCS#7 Pakiet certyfikatu PKCS#7 pacote de certificação PKCS#7 Pacote de certificados PKCS#7 Pachet certificat PKCS#7 пакет сертификатов PKCS#7 Zväzok certifikátov PKCS#7 Datoteka potrdila PKCS#7 ПКЦС#7 пакет уверења PKCS#7-certifikatsamling PKCS#7 sertifika paketi комплект сертифікатів PKCS#7 Bó chứng nhận PKCS#7 PKCS#7 证书束 PKCS#7 憑證綁包 PKCS Public-Key Cryptography Standards PKCS#12 certificate bundle رزمة الشهادة PKCS#12 Viazka sertyfikataŭ PKCS#12 Сбор със сертификати — PKCS#12 conjunt de certificats PKCS#12 svazek certifikátů PKCS#12 PKCS#12-certifikatbundt PKCS#12-Zertifikatspaket Πακέτο ψηφιακών πιστοποιητικών PKCS#12 PKCS#12 certificate bundle ligaĵo de PKCS#12-atestiloj lote de certificados PCKS#12 PKCS#12 zertifikazio sorta PKCS#12-varmennenippu PKCS#12 váttanar bundi lot de certificats PKCS#12 cuach theastas PKCS#12 paquete de certificado PKCS#12 בקשה מוסמכת PKCS#12 PKCS#12 paket vjerodajnica PKCS#12-tanúsítványcsomag Pacchetto de certificatos PKCS#12 Bundel sertifikat PKCS#12 Bundle certificato PKCS#12 PKCS#12 証明書 PKCS#12 сертификаттар дестесі PKCS#12 인증서 묶음 PKCS#12 liudijimų ryšulys PKCS#12 sertifikātu saišķis Sijil PKCS#12 PKCS#12 sertifikathaug PKCS#12-certificaatbundel PKCS#12-sertifikatbunt lòt de certificats PKCS#12 Pakiet certyfikatu PKCS#12 pacote de certificação PKCS#12 Pacote de certificados PKCS#12 Certificat împachetat PKCS#12 пакет сертификатов PKCS#12 Zväzok certifikátov PKCS#12 Datoteka potrdila PKCS#12 Bundle çertifikate PKCS#12 ПКЦС#12 пакет уверења PKCS#12-certifikatsamling PKCS#12 sertifika paketi комплект сертифікатів PKCS#12 Bó chứng nhận PKCS#12 PKCS#12 证书束 PKCS#12 憑證檔綁包 PKCS Public-Key Cryptography Standards PlanPerfect spreadsheet جدول PlanPerfect Raźlikovy arkuš PlanPerfect Таблица — PlanPerfect full de càlcul de PlanPerfect sešit PlanPerfect PlanPerfect-regneark PlanPerfect-Tabelle Φύλλο εργασίας PlanPerfect PlanPerfect spreadsheet hoja de cálculo de PlanPerfect PlanPerfect kalkulu-orria PlanPerfect-taulukko PlanPerfect rokniark feuille de calcul PlanPerfect scarbhileog PlanPerfect folla de cálculo de PlanPerfect גליון נתונים של PlanPerfect PlanPerfect proračunska tablica PlanPerfect táblázat Folio de calculo PlanPerfect Lembar sebar PlanPerfect Foglio di calcolo PlanPerfect PlanPerfect スプレッドシート PlanPerfect электрондық кестесі PlanPerfect 스프레드시트 PlanPerfect skaičialentė PlanPerfect izklājlapa PlanPerfect-regneark PlanPerfect-rekenblad PlanPerfect-rekneark fuèlh de calcul PlanPerfect Arkusz PlanPerfect folha de cálculo PlanPerfect Planilha do PlanPerfect Foaie de calcul PlanPerfect электронная таблица PlanPerfect Zošit PlanPerfect Preglednica PlanPerfect Fletë llogaritjesh PlanPerfect табела План Перфекта PlanPerfect-kalkylblad PlanPerfect çalışma sayfası ел. таблиця PlanPerfect Bảng tính PlanPerfect PlanPerfect 工作簿 PlanPerfect 試算表 Pocket Word document مستند Pocket Word Документ — Pocket Word document de Pocket Word dokument Pocket Word Pocket Word-dokument Pocket-Word-Dokument Έγγραφο Pocket Word Pocket Word document documento de Pocket Word Pocket Word dokumentua Pocket Word -asiakirja Pocket Word skjal document Pocket Word cáipéis Pocket Word documento de Pocket Word מסמך של Pocket Word Pocket Word dokument Pocket Word dokumentum Documento Pocket Word Dokumen Pocket Word Documento Pocket Word Pocket Word ドキュメント Pocket Word құжаты Pocket Word 문서 Pocket Word dokumentas Pocket Word dokuments Pocket Word-document document Pocket Word Dokument Pocket Word documento Pocket Word Documento do Pocket Word Document Pocket Word документ Pocket Word Dokument Pocket Word Dokument Pocket Word документ Покет Ворда Pocket Word-dokument Pocket Word belgesi документ Pocket Word Tài liệu Pocket Word Pocket Word 文档 Pocket Word 文件 profiler results نتائج المحلل profiler nəticələri vyniki profilera Резултати от анализатора resultats de profiler výsledky profileru canlyniadau proffeilio profileringsresultater Profiler-Ergebnisse Αποτελέσματα μετρήσεων για την εκτέλεση προγράμματος profiler results resultoj de profililo resultados del perfilador profiler-aren emaitzak profilointitulokset résultats de profileur torthaí próifíleora resultados do perfilador תוצאות מאבחן Rezultati profila profilírozó-eredmények Resultatos de profilator hasil profiler Risultati profiler プロファイラー結果 прифильдеу нәтижелері 프로파일러 결과 profiliklio rezultatai profilētāja rezultāti Hasil pemprofil profileingsresultat profiler-resultaten profileringsresultat resultats de perfilador Wyniki profilowania resultados de análise de perfil Resultados do profiler rezultate profiler результаты профилирования Výsledky profilera rezultati profilirnika Rezultate të profiluesit резултати профилатора profilerarresultat profil sonuçları результати профілювання kết quả nét hiện trạng profiler 结果 硬體資訊產生器成果 Pathetic Writer document مستند Pathetic Writer Dakument Pathetic Writer Документ — Pathetic Writer document de Pathetic Writer dokument Pathetic Writer Pathetic Writer-dokument Pathetic-Writer-Dokument Έγγραφο Pathetic Writer Pathetic Writer document dokumento de Pathetic Writer documento de Pathetic Writer Pathetic Writer dokumentua Pathetic Writer -asiakirja Pathetic Writer skjal document Pathetic Writer cáipéis Pathetic Writer documento de Pathetic Writer מסמך של Pathetic Writer Pathetic Writer dokument Pathetic Writer-dokumentum Documento Pathetic Writer Dokumen Pathetic Writer Documento Pathetic Writer Pathetic Writer ドキュメント Pathetic Writer құжаты Pathetic Writer 문서 Pathetic Writer dokumentas Pathetic Writer dokuments Dokumen Pathetic Writer Pathetic Writer-dokument Pathetic Writer-document Pathetic Writer-dokument document Pathetic Writer Dokument Pathetic Writer documento do Pathetic Writer Documento do Pathetic Writer Document Pathetic Writer документ Pathetic Writer Dokument Pathetic Writer Dokument Pathetic Writer Dokument Pathetic Writer документ Патетичног Писца Pathetic Writer-dokument Pathetic Writer belgesi документ Pathetic Writer Tài liệu Pathetic Writer Pathetic Writer 文档 Pathetic Writer 文件 Python bytecode Python bytecode Python bayt kodu Bajtavy kod Python Байт код — Python bytecode de Python bajtový kód Python Côd beit Python Pythonbytekode Python-Bytecode Συμβολοκώδικας Python Python bytecode Python-bajtkodo bytecode de Python Python byte-kodea Python-tavukoodi Python býtkota bytecode Python beartchód Python bytecode de Python Bytecode של Python Python bajt kôd Python-bájtkód Codice intermediari Python Kode bita Python Bytecode Python Python バイトコード Python байткоды 파이썬 바이트코드 Python baitinis kodas Python bitkods Kodbait Python Python-bytekode Python-bytecode Python-bytekode bytecode Python Kod bajtowy Python código binário Python Código compilado Python Bytecode Python байт-код Python Bajtový kód Python Datoteka bitne kode Python Bytecode Python Питонов бајтни ко̂д Python-bytekod Python bayt kodu байт-код Python Mã byte Python Python 字节码 Python 位元組碼 QtiPlot document document QtiPlot dokument GtiPlot QtiPlot-dokument QtiPlot-Dokument Έγγραφο QtiPlot QtiPlot document documento de QtiPlot QtiPlot dokumentua QtiPlot-asiakirja document QtiPlot Documento de QtiPilot מסמך QtiPlot QtiPlot dokument QtiPlot dokumentum Documento QtiPlot Dokumen QtiPlot Documento QtiPlot QtiPlot ドキュメント QtiPlot құжаты QtiPlot 문서 QtiPlot dokuments document QtiPlot Dokument QtiPlot documento QtiPlot Documento do QtiPlot документ QtiPlot Dokument QtiPlot Dokument QtiPlot КутиПлот документ QtiPlot-dokument QtiPlot belgesi документ QtiPlot QtiPlot 文档 QtiPlot 文件 Quattro Pro spreadsheet جدول Quattro Pro Raźlikovy arkuš Quattro Pro Таблица — Quattro Pro full de càlcul de Quattro Pro sešit Quattro Pro Quattro Pro-regneark Quattro-Pro-Tabelle Λογιστικό φύλλο Quattro Pro Quattro Pro spreadsheet sterntabelo de Quattro Pro hoja de cálculo de Quattro Pro Quattro Pro kalkulu-orria Quattro Pro -taulukko Quattro Pro rokniark feuille de calcul Quattro Pro scarbhileog Quattro Pro folla de cálculo Quattro Pro גליון נתונים של Quattro Pro Quattro Pro proračunska tablica Quattro Pro-munkafüzet Folio de calculo Quattro Pro Lembar sebar Quattro Pro Foglio di calcolo Quattro Pro Quattro Pro スプレッドシート Quattro Pro электрондық кестесі Quattro Pro 스프레드시트 Quattro Pro skaičialentė Quattro Pro izklājlapa Hamparan Quatro Pro Quattro Pro-regneark Quattro Pro-rekenblad Quattro Pro-rekneark fuèlh de calcul Quattro Pro Arkusz Quattro Pro folha de cálculo Quattro Pro Planilha do Quattro Pro Foaie de calcul Quattro Pro электронная таблица Quattro Pro Zošit Quattro Pro Preglednica Quattro Pro Fletë llogaritjesh Quattro Pro Кватро Про табела Quattro Pro-kalkylblad Quattro Pro çalışma sayfası ел. таблиця Quattro Pro Bảng tính Quattro Pro Quattro Pro 工作簿 Quattro Pro 試算表 QuickTime metalink playlist قائمة تشغيل QuickTime metalink śpis metaspasyłak na pieśni QuickTime Списък за изпълнение — QuickTime llista de reproducció de metaenllaços QuickTime seznam k přehrání metalink QuickTime QuickTime metalink-afspilningsliste QuickTime-Metalink-Wiedergabeliste Λίστα αναπαραγωγής metalinks QuickTime QuickTime metalink playlist lista de reproducción de metaenlaces QuickTime QuickTime meta-esteken erreprodukzio-zerrenda QuickTime metalink -soittolista QuickTime metaleinkju avspælingarlisti liste de lecture metalink QuickTime seinmliosta meiteanasc QuickTime lista de reprodución de metaligazóns QuickTime רשימת השמעה מקושרת של QuickTime QuickTime meta poveznica popisa izvođenja QuickTime metalink lejátszólista Lista de selection Metalink QuickTime Senarai berkas taut meta QuickTime Playlist metalink QuickTime QuickTime メタリンク再生リスト QuickTime метасілтемелер ойнау тізімі 퀵타임 메타링크 재생 목록 QuickTime metanuorodos grojaraštis QuickTime metasaites repertuārs QuickTime metalink-spilleliste QuickTime metalink-afspeellijst QuickTime metalink-speleliste lista de lectura metalink QuickTime Lista odtwarzania metaodnośników QuickTime lista de reprodução QuickTime metalink Lista de reprodução metalink do QuickTime Listă cu metalegături QuickTime список воспроизведения мета-ссылок QuickTime Zoznam skladieb metalink QuickTime Seznam predvajanja QuickTime Listë titujsh metalink QuickTime списак нумера мета везе Квик Тајма QuickTime-metalänkspellista QuickTime metalink çalma listesi список відтворення QuickTime metalink Danh mục nhạc siêu liên kết Quicktime QuickTime 元链接播放列表 QuickTime metalink 播放清單 Quicken document مستند Quicken Quicken sənədi Dakument Quicken Документ — Quicken document Quicken dokument Quicken Dogfen Quicken Quickendokument Quicken-Dokument Έγγραφο Quicken Quicken document Quicken-dokumento documento de Quicken Quicken dokumentua Quicken-asiakirja Quicken skjal document Quicken cáipéis Quicken documento de Quicken מסמך של Quicken Quicken dokument Quicken-dokumentum Documento Quicken Dokumen Quicken Documento Quicken Quicken ドキュメント Quicken құжаты Quicken 문서 Quicken dokumentas Quicken dokuments Dokumen Quicken Quicken-dokument Quicken-document Quicken-dokument document Quicken Dokument Quicken documento Quicken Documento do Quicken Document Quicken документ Quicken Dokument Quicken Dokument Quicken Dokument Quicken Квикен документ Quicken-dokument Quicken belgesi документ Quicken Tài liệu Quicken Quicken 文档 Quicken 文件 RAR archive أرشيف RAR Archiŭ RAR Архив — RAR arxiu RAR archiv RAR Archif RAR RAR-arkiv RAR-Archiv Συμπιεσμένο αρχείο RAR RAR archive RAR-arkivo archivador RAR RAR artxiboa RAR-arkisto RAR skjalasavn archive RAR cartlann RAR ficheiro RAR ארכיון RAR RAR arhiva RAR-archívum Archivo RAR Arsip RAR Archivio RAR RAR アーカイブ RAR архиві RAR 압축 파일 RAR archyvas RAR arhīvs Arkib RAR RAR-arkiv RAR-archief RAR-arkiv archiu RAR Archiwum RAR arquivo RAR Pacote RAR Arhivă RAR архив RAR Archív RAR Datoteka arhiva RAR Arkiv RAR РАР архива RAR-arkiv RAR arşivi архів RAR Kho nén RAR RAR 归档文件 RAR 封存檔 RAR Roshal ARchive DAR archive أرشيف DAR Archiŭ DAR Архив — DAR arxiu DAR archiv DAR DAR-arkiv DAR-Archiv Συμπιεσμένο αρχείο DAR DAR archive DAR-arkivo archivador DAR DAR artxiboa DAR-arkisto DAR skjalasavn archive DAR cartlann DAR arquivo DAR ארכיון DAR DAR arhiva DAR archívum Archivo DAR Arsip DAR Archivio DAR DAR アーカイブ DAR არქივი DAR архиві DAR 묶음 파일 DAR archyvas DAR arhīvs DAR-arkiv DAR-archief DAR-arkiv archiu DAR Archiwum DAR arquivo DAR Pacote DAR Arhivă DAR архив DAR Archív DAR Datoteka arhiva DAR Arkiv DAR ДАР архива DAR-arkiv DAR arşivi архів DAR Kho nén DAR DAR 归档文件 DAR 封存檔 Alzip archive أرشيف Alzip Archiŭ Alzip Архив — alzip arxiu Alzip archiv Alzip Alziparkiv Alzip-Archiv Συμπιεσμένο αρχείο Alzip Alzip archive Alzip-arkivo archivador Alzip Alzip artxiboa Alzip-arkisto Alsip skjalasavn archive alzip cartlann Alzip arquivo Alzip ארכיון Alzip Alzip arhiva Alzip archívum Archivo Alzip Arsip Alzip Archivio Alzip Alzip アーカイブ Alzip არქივი Alzip архиві 알집 압축 파일 Alzip archyvas Alzip arhīvs Alzip-arkiv Alzip-archief Alzip-arkiv archiu alzip Archiwum alzip arquivo Alzip Pacote Alzip Arhivă Alzip архив ALZIP Archív Alzip Datoteka arhiva Alzip Arkiv Alzip Алзип архива Alzip-arkiv Alzip arşivi архів Alzip Kho nén Alzip Alzip 归档文件 Alzip 封存檔 rejected patch رقعة مرفوضة niepryniaty patch Отхвърлен файл с кръпка pedaç rebutjat odmítnutá záplata afvist tekstlap Abgelehnter Patch Διόρθωση που απορρίφθηκε rejected patch reĵeta flikaĵo parche rechazado baztertutako bide-izena hylättyjen muutosten tiedosto vrakað rætting correctif rejeté paiste diúltaithe parche rexeitado טלאי שנדחה odbijena zakrpa visszautasított folt Patch rejectate patch ditolak Patch rifiutata 拒否されたパッチ алынбаған патч 거부된 패치 파일 atmestas lopas noraidītais ceļš Tampungan ditolak avvist patchfil verworpen patch avvist programfiks correctiu regetat Odrzucona łata patch rejeitado Arquivo de patch rejeitado petec respsins отвергнутый патч Odmietnutá záplata zavrnjen popravek Patch i kthyer mbrapsht одбијена закрпа avvisad programfix reddedilmiş yama відхилена латка đắp vá bị từ chối 拒绝的补丁 回絕的修補 RPM package حزمة RPM Pakunak RPM Пакет — RPM paquet RPM balíček RPM RPM-pakke RPM-Paket Πακέτο RPM RPM package RPM-pakaĵo paquete RPM RPM paketea RPM-paketti RPM pakki paquet RPM pacáiste RPM paquete RFM חבילת RPM RPM paket RPM-csomag Pacchetto RPM Paket RPM Pacchetto RPM RPM パッケージ RPM дестесі RPM 패키지 RPM paketas RPM pakotne Pakej RPM RPM-pakke RPM-pakket RPM-pakke paquet RPM Pakiet RPM pacote RPM Pacote RPM Pachet RPM пакет RPM Balík RPM Datoteka paketa RPM Paketë RPM РПМ пакет RPM-paket RPM paketi пакунок RPM Gói RPM RPM 软件包 RPM 軟體包 Source RPM package paquet RPM de codi font zdrojový balíček RPM Kilde RPM-pakke Quell-RPM-Paket Πακέτο πηγής RPM Source RPM package paquete de fuente RPM Iturburu RPM paketea RPM-lähdepaketti paquet source RPM Paquete RPM de fontes חבילת מקור RPM RPM paket izvora Forrás RPM-csomag Pacchetto de fonte RPM Paket RPM sumber Pacchetto sorgente RPM ソース RPM パッケージ RPM бастапқы код дестесі 소스 RPM 패키지 Avota RPM pakotne paquet font RPM Źródłowy pakiet RPM pacote origem RPM Pacote fonte RPM пакет RPM с исходным кодом Zdrojový balík RPM Paket izvorne kode RPM изворни РПМ пакет Käll-RPM-paket Kaynak RPM paketi пакунок RPM з початковим кодом 源码 RPM 软件包 來源 RPM 軟體包 Ruby script سكربت روبي Skrypt Ruby Скрипт — Ruby script Ruby skript Ruby Rubyprogram Ruby-Skript Δέσμη ενεργειών Ruby Ruby script Ruby-skripto secuencia de órdenes en Ruby Ruby script-a Ruby-komentotiedosto Ruby boðrøð script Ruby script Ruby Script de Ruby תסריט Ruby Ruby skripta Ruby-parancsfájl Script Ruby Skrip Ruby Script Ruby Ruby スクリプト Ruby сценарийі 루비 스크립트 Ruby scenarijus Ruby skripts Skrip Ruby Ruby-skript Ruby-script Ruby-skript escript Ruby Skrypt Ruby script Ruby Script Ruby Script Ruby сценарий Ruby Skript Ruby Skriptna datoteka Ruby Script Ruby Руби скрипта Ruby-skript Ruby betiği скрипт Ruby Văn lệnh Ruby Ruby 脚本 Ruby 指令稿 Markaby script سكربت Markaby Skrypt Markaby Скрипт — Markaby script Markaby skript Markaby Markabyprogram Markaby-Skript Δέσμη ενεργειών Markaby Markaby script Markaby-skripto secuencia de órdenes en Markaby Markaby script-a Markaby-komentotiedosto Markaby boðrøð script Markaby script Markaby Script de Markaby תסריט Markby Markaby skripta Markaby parancsfájl Script Markaby Skrip Markaby Script Markaby Markaby スクリプト Markaby-ის სცენარი Markaby сценарийі Markaby 스크립트 Markaby scenarijus Markaby skripts Markaby-skript Markaby-script Markaby-skript escript Markaby Skrypt Markaby script Markaby Script Markaby Script Markaby сценарий Markaby Skript Markaby Skriptna datoteka Markaby Script Markaby Маркаби скрипта Markaby-skript Markaby betiği скрипт Markaby Văn lệnh Markaby RMarkaby 脚本 Markaby 指令稿 Rust source code codi font en Rust zdrojový kód v jazyce Rust Rust-kildekode Rust-Quelltext Πηγαίος κώδικας Rust Rust source code código fuente en Rust Rust iturburu-kodea Rust-lähdekoodi Rust izvorni kôd Rust forrásfájl Codice-fonte Rust Kode program Rust Codice sorgente Rust Rust бастапқы коды Rust 소스 코드 còde font Rust Kod źródłowy Rust código origem Rust Código-fonte Rust исходный код Rust Zdrojový kód Rust Раст изворни ко̂д Rust-källkod Rust kaynak kodu вихідний код мовою Rust Rust 源代码 Rust 源碼 SC/Xspread spreadsheet جدول SC/Xspread Raźlikovy arkuš SC/Xspread Таблица — SC/Xspread full de càlcul de SC/Xspread sešit SC/Xspread SC/Xspread-regneark SX/Xspread-Tabelle Λογιστικό φύλλο SC/Xspread SC/Xspread spreadsheet SC/Xspread-kalkultabelo hoja de cálculo SC/Xspread SC/Xspread kalkulu-orria SC/Xspread-taulukko SC/Xspread rokniark feuille de calcul SC/Xspread scarbhileog SC/Xspread folla de cálculo SC/Xspread גליון נתונים של SC/Xspread SC/Xspread proračunska tablica SC/Xspread táblázat Folio de calculo SC/Xspread Lembar sebar SC/Xspread Foglio di calcolo SC/Xspread SC/Xspread スプレッドシート SC/Xspread электрондық кестесі SC/Xspread 스프레드시트 SC/Xspread skaičialentė SC/Xspread izklājlapa SC/Xspread-regneark SC/Xspread-rekenblad SC/Xspread-rekneark fuèlh de calcul SC/Xspread Arkusz SC/Xspread folha de cálculo SC/Xspread Planilha do SC/Xspread Foaie de calcul SC/Xspread электронная таблица SC/Xspread Zošit SC/Xspread Preglednica SC/Xspread Fletë llogaritjesh SC/Xspread табела СЦ/Икс-табеле SC/Xspread-kalkylblad SC/Xspread çalışma sayfası ел. таблиця SC/Xspread Bảng tính SC/Xspread SC/Xspread 工作簿 SC/Xspread 試算表 shell archive أرشيف شِل qabıq arxivi archiŭ abałonki Архив на обвивката arxiu de shell archiv shellu archif plisgyn skalarkiv Shell-Archiv Αρχείο κέλυφους shell archive ŝel-arkivo archivador shell shell artxiboa komentotulkkiarkisto skel savn archive shell cartlann bhlaoisce ficheiro shell ארכיון מעטפת arhiva ljuske héjarchívum Archivo de shell arsip shell Archivio shell シェルアーカイブ қоршам архиві 셸 압축 파일 shell archyvas čaulas arhīvs Arkib shell skallarkiv shell-archief skal-arkiv archiu shell Archiwum powłoki arquivo de terminal Pacote shell arhivă shell архив оболочки UNIX Archív shellu lupinski arhiv Arkiv shell архива љуске skalarkiv kabuk arşivi архів оболонки kho trình bao shell 归档文件 shell 封存檔 libtool shared library مكتبة libtool المشتركة supolnaja biblijateka libtool Споделена библиотека — libtool biblioteca compartida libtool sdílená knihovna libtool libtool delt bibliotek Gemeinsame libtool-Bibliothek Κοινόχρηστη βιβλιοθήκη libtool libtool shared library biblioteca compartida de libtool libtool partekatutako liburutegia jaettu libtool-kirjasto libtool felagssavn bibliothèque partagée libtool comhleabharlann libtool biblioteca compartida de libtool ספרייה משותפת של libtool libtool dijeljena biblioteka libtool osztott programkönyvtár Bibliotheca commun Libtool pustaka bersama libtool Libreria condivisa libtool libtool 共有ライブラリ libtool ортақ жинағы libtool 공유 라이브러리 libtool bendroji biblioteka libtool koplietotā bibliotēka libtool delt bibliotek gedeelde libtool-bibliotheek libtool delt bibliotek bibliotèca partejada libtool Biblioteka współdzielona libtool biblioteca partilhada libtool Biblioteca compartilhada libtool bibliotecă partajată libtool разделяемая библиотека libtool Zdieľaná knižnica libtool Souporabna knjižnica libtool Librari e përbashkët libtool дељена библиотека библ-алата delat libtool-bibliotek libtool paylaşımlı kitaplığı спільна бібліотека libtool thư viện dùng chung libtool libtool 共享库 libtool 共享函式庫 shared library مكتبة مشتركة bölüşülmüş kitabxana supolnaja biblijateka Споделена библиотека biblioteca compartida sdílená knihovna llyfrgell wedi ei rhannu delt bibliotek Gemeinsame Bibliothek Αρχείο κοινόχρηστης βιβλιοθήκης shared library dinamike bindebla biblioteko biblioteca compartida partekatutako liburutegia jaettu kirjasto felagssavn bibliothèque partagée comhleabharlann biblioteca compartida ספרייה משותפת dijeljena biblioteka osztott programkönyvtár Bibliotheca commun pustaka bersama Libreria condivisa 共有ライブラリ бөлісетін библиотека 공유 라이브러리 bendroji biblioteka koplietotā bibliotēka Pustaka terkongsi delt bibliotek gedeelde bibliotheek delt bibliotek bibliotèca partejada Biblioteka współdzielona biblioteca partilhada Biblioteca compartilhada bibliotecă partajată разделяемая библиотека Zdieľaná knižnica souporabljena knjižnica Librari e përbashkët дељена библиотека delat bibliotek paylaşımlı kitaplık спільна бібліотека thư viện dùng chung 共享库 共享函式庫 shell script سكربت شِل qabıq skripti skrypt abałonki Скрипт на обвивката script de shell skript shellu sgript plisgyn skalprogram Shell-Skript Δέσμη ενεργειών κελύφους shell script ŝelskripto secuencia de órdenes en shell shell script-a komentotulkin komentotiedosto skel boðrøð script shell script bhlaoisce script de shell תסריט מעטפת skripta ljuske héj-parancsfájl Script de shell skrip shell Script shell シェルスクリプト қоршам сценарийі 셸 스크립트 shell scenarijus čaulas skripts Skrip shell skallskript shellscript skalskript escript shell Skrypt powłoki script de terminal Script shell script shell сценарий оболочки UNIX Skript shellu lupinski skript Script shell скрипта љуске skalskript kabuk betiği скрипт оболонки văn lệnh trình bao shell 脚本 shell 指令稿 Shockwave Flash file ملف Shockwave Flash Fajł Shockwave Flash Файл — Shockwave Flash fitxer Shockwave Flash soubor Shockwave Flash Shockwave Flash-fil Shockwave-Flash-Datei Αρχείο Shockwave Flash Shockwave Flash file dosiero de Shockwave Flash archivo Shockwave Flash Shockwave Flash fitxategia Shockwave Flash -tiedosto Shockwave Flash fíla fichier Shockwave Flash comhad Shockwave Flash ficheiro sockwave Flash קובץ של Shockwave Flash Shockwave Flash datoteka Shockwave Flash-fájl File Shockwave Flash Berkas Shockwave Flash File Shockwave Flash Shockwave Flash ファイル Shockwave Flash файлы Shockwave 플래시 파일 Shockwave Flash failas Shockwave Flash datne Fail Shockwave Flash Shockwave Flash-fil Shockwave Flash-bestand Shockwave Flash-fil fichièr Shockwave Flash Plik Shockwave Flash ficheiro Shockwave Flash Arquivo Shockwave Flash Fișier Shockwave Flash файл Shockwave Flash Súbor Shockwave Flash Datoteka Shockwave Flash File Flash Shockwave Шоквејв Флеш датотека Shockwave Flash-fil Shockwave Flash dosyası файл Shockwave Flash Tập tin Flash Shockwave Shockwave Flash 文件 Shockwave Flash 檔案 Shorten audio Shorten سمعي Aŭdyjo Shorten Аудио — Shorten àudio de Shorten zvuk Shorten Shortenlyd Shorten-Audio Ήχος Shorten Shorten audio Shorten-sondosiero sonido Shorten Shorten audioa Shorten-ääni Shorten ljóður audio Shorten fuaim Shorten son Shorten שמע של Shorten Shorten audio Shorten hang Audio Shorten Audio Shorten Audio Shorten Shorten オーディオ Shorten аудиосы Shorten 오디오 Shorten garso įrašas Shorten audio Shorten lyd Shorten-audio Shorten-lyd àudio Shorten Plik dźwiękowy Shorten áudio Shorten Áudio Shorten Audio Shorten аудио Shorten Zvuk Shorten Zvočna datoteka Shorten Audio Shorten Шортен звук Shorten-ljud Kısaltılmış ses звук Shorten Âm thanh Shorten Shorten 音频 Shorten 音訊 Siag spreadsheet جدول Siag Raźlikovy arkuš Siag Таблица — Siag full de càlcul Siag sešit Siag Siagregneark Siag-Tabelle Λογιστικό φύλλο Siag Siag spreadsheet Siag-kalkultabelo hoja de cálculo de Siag Siag kalkulu-orria Siag-taulukko Siag rokniark feuille de calcul Siag scarbhileog Siag folla de cálculo de Siag גליון נתונים של Siag Siag proračunska tablica Siag-munkafüzet Folio de calculo Siag Lembar sebar Siag Foglio di calcolo Siag Siag スプレッドシート Siag электрондық кестесі Siag 스프레드시트 Siag skaičialentė Siag izklājlapa Hamparan Siag Siag-regneark Siag-rekenblad Siag-rekneark fuèlh de calcul Siag Arkusz Siag folha de cálculo Siag Planilha do Siag Foaie de calcul Siag электронная таблица Siag Zošit Siag Preglednica Siag Fletë llogaritjesh Siag Сјаг табела Siag-kalkylblad Siag çalışma sayfası ел. таблиця Siag Bảng tính Slag Siag 工作簿 Siag 試算表 Skencil document مستند Skencil Dakument Skencil Документ — Skencil document Skencil dokument Skencil Skencildokument Skencil-Dokument Έγγραφο Skencil Skencil document Skencil-dokumento documento de Skencil Skencil dokumentua Skencil-asiakirja Skencil skjal document Skencil cáipéis Skencil documento Skencil מסמך Skencil Skencil dokument Skencil-dokumentum Documento Skencil Dokumen Skencil Documento Skencil Skencil ドキュメント Skencil құжаты Skencil 문서 Skencil dokumentas Skencil dokuments Skencil-document Skencil-dokument document Skencil Dokument Skencil documento Skencil Documento do Skencil Document Skencil документ Skencil Dokument Skencil Dokument Skencil Dokument Skencil Скенцил документ Skencil-dokument Skencil belgesi документ Skencil Tài liệu Skencil Skencil 文档 Skencil 文件 Stampede package حزمة Stampede Stampede paketi Pakunak Stampede Пакет — Stampede paquet Stampede balíček Stampede Pecyn Stampede Stampedepakke Stampede-Paket Πακέτο Stampede Stampede package Stampede-pakaĵo paquete Stampede Stampede paketea Stampede-paketti Stampede pakki paquet Stampede pacáiste Stampede paquete Stampede חבילה של Stampede Stampede paket Stampede-csomag Pacchetto Stampede Paket Stampede Pacchetto Stampede Stampede パッケージ Stampede дестесі Stampete 패키지 Stampede paketas Stampede pakotne Pakej Stampede Stampede-pakke Stampede-pakket Stampede-pakke paquet Stampede Pakiet Stampede pacote Stampede Pacote Stampede Pachet Stampede пакет Stampede Balíček Stampede Datoteka paketa Stampede Paketë Stampede Стампеде пакет Stampede-paket Stampede paketi пакунок Stampede Gói Stampede Stampede 软件包 Stampede 軟體包 SG-1000 ROM Master System ROM Game Gear ROM Super NES ROM Super NES ROM Super Nintendo ROM ROM — Super NES ROM de Super NES ROM pro Super Nintendo Super NES-rom Super-NES-ROM Super NES ROM Super NES ROM ROM de Super NES Super Nintendo-ko ROMa Super Nintendo -ROM Super NES ROM ROM Super Nintendo ROM Super NES ROM de Super NES ROM של Super NES Super NES ROM Super NES ROM ROM pro Super Nintendo Memori baca-saja Super Nintendo ROM Super Nintendo スーパーファミコン ROM Super NES ROM 수퍼 NES 롬 Super NES ROM Super NES ROM Super Nintendo ROM Super Nintendo Super NES-ROM ROM Super Nintendo Plik ROM konsoli SNES ROM Super Nintendo ROM do Super Nintendo ROM Super Nintendo Super NES ROM ROM pre Super Nintendo Bralni pomnilnik Super NES ROM Super NES Супер НЕС РОМ Super NES-rom Super NES ROM ППП Super NES ROM Super Nintendo Super NES ROM 超級任天堂 ROM StuffIt archive أرشيف StuffIt Archiŭ StuffIt Архив — StuffIt arxiu StuffIt archiv StuffIt StuffIt-arkiv StuffIt-Archiv Συμπιεσμένο αρχείο StuffIt StuffIt archive StuffIt-arkivo archivador de StuffIt StuffIt artxiboa StuffIt-arkisto StuffIt skjalasavn archive StuffIt cartlann StuffIt arquivo StuffIt ארכיון של StuffIt StuffIt arhiva StuffIt-archívum Archivo StuffIt Arsip StuffIt Archivio StuffIt StuffIt アーカイブ StuffIt архиві StuffIt 압축 파일 StuffIt archyvas StuffIt arhīvs StuffIt arkiv StuffIt-archief StuffIt-arkiv archiu StuffIt Archiwum StuffIt arquivo StuffIt Pacote StuffIt Arhivă StuffIt архив StuffIt Archív StuffIt Datoteka arhiva StuffIt Arkiv StuffIt Стаф Ит архива StuffIt-arkiv StuffIt arşivi архів StuffIt Kho nén Stuffit Macintosh StuffIt 归档文件 StuffIt 封存檔 SubRip subtitles ترجمات SubRip Subtytry SubRip Субтитри — SubRip subtítols SubRip titulky SubRip SubRip-undertekster SubRip-Untertitel Υπότιτλοι SubRip SubRip subtitles SubRip-subtekstoj subtítulos SubRip SubRip azpitituluak SubRip-tekstitykset SubRip undirtekstir sous-titres SubRip fotheidil SubRip subtítulos SubRip כתוביות של SubRip SubRip titlovi SubRip feliratok Subtitulos SubRip Subjudul SubRip Sottotitoli SubRip SubRip 字幕 SubRip субтитрлары SubRip 자막 파일 SubRip subtitrai SubRip subtitri SubRip undertekst SubRip-ondertitels SubRip-teksting sostítols SubRip Napisy SubRip legendas SubRip Legendas SubRip Subtitrare SubRip субтитры SubRip Titulky SubRip Datoteka podnapisov SubRip Nëntituj SubRip Суб Рип преводи SubRip-undertexter SubRip altyazıları субтитри SubRip Phụ đề SubRip SubRip 字幕 SubRip 字幕 WebVTT subtitles Субтитри — WebVTT subtítols WebVTT titulky WebVTT WebVTT-undertekster WebVTT-Untertitel Υπότιτλοι WebVTT WebVTT subtitles subtítulos WebVTT WebVTT azpitituluak WebVTT-tekstitykset sous-titres WebVTT subtítulos WebVTT כתוביות WebVTT WebVTT titlovi WebVTT feliratok Subtitulos WebVTT Subtitel WebVTT Sottotitoli WebVTT WebVTT サブタイトル WebVTT ქვეტიტრები WebVTT субтитрлары WebVTT 자막 WebVTT subtitri WebVTT ondertitels sostítols WebVTT Napisy WebVTT legendas WebVTT Legendas WebVTT субтитры WebVTT Titulky WebVTT Podnapisi WebVTT Веб ВТТ преводи WebVTT-undertexter WebVTT altyazıları субтитри WebVTT WebVTT 字幕 WebVTT 字幕 VTT Video Text Tracks SAMI subtitles ترجمات SAMI Subtytry SAMI Субтитри — SAMI subtítols SAMI titulky SAMI SAMI-undertekster SAMI-Untertitel Υπότιτλοι SAMI SAMI subtitles SAMI-subtekstoj subtítulos SAMI SAMI azpitituluak SAMI-tekstitykset SAMI undirtekstir sous-titres SAMI fotheidil SAMI subtítulos SAMI כתוביות SAMI SAMI titlovi SAMI feliratok Subtitulos SAMI Subjudul SAMI Sottotitoli SAMI SAMI 字幕 SAMI субтитрлары SAMI 자막 파일 SAMI subtitrai SAMI subtitri SAMI undertekst SAMI-ondertitels SAMI teksting sostítols SAMI Napisy SAMI legendas SAMI Legendas SAMI Subtitrări SAMI субтитры SAMI Titulky SAMI Datoteka podnapisov SAMI Nëntituj SAMI САМИ преводи SAMI-undertexter SAMI altyazıları субтитри SAMI Phụ đề SAMI SAMI 字幕 SAMI 字幕 SAMI Synchronized Accessible Media Interchange MicroDVD subtitles ترجمات MicroDVD Subtytry MicroDVD Субтитри — MicroDVD subtítols MicroDVD titulky MicroDVD MicroDVD-undertekster MicroDVD-Untertitel Υπότιτλοι MicroDVD MicroDVD subtitles MicroDVD-subtekstoj subtítulos de MicroDVD MicroDVD azpitituluak MicroDVD-tekstitykset MicroDVD undirtekstir sous-titres MicroDVD fotheidil MicroDVD subtítulos de MicroDVD כתוביות של MicroDVD MicroDVD titlovi MicroDVD feliratok Subtitulos MicroDVD Subjudul MicroDVD Sottotitoli MicroDVD MicroDVD 字幕 MicroDVD-ის ქვეტიტრები MicroDVD субтитрлары MicroDVD 자막 파일 MicroDVD subtitrai MicroDVD subtitri MicroDVD undertekst MicroDVD-ondertitels MicroDVD-teksting sostítols MicroDVD Napisy MicroDVD legendas MicroDVD Legendas MicroDVD Subtitrări MicroDVD субтитры MicroDVD Titulky MicroDVD Datoteka podnapisov MicroDVD Nëntituj MicroDVD Микро ДВД преводи MicroDVD-undertexter MicroDVD altyazısı субтитри MicroDVD Phụ đề MicroDVD MicroDVD 字幕 MicroDVD 字幕 MPSub subtitles ترجمات MPSub Subtytry MPSub Субтитри — MPSub subtítols MPSub titulky MPSub MPSub-undertekster MPSub-Untertitel Υπότιτλοι MPSub MPSub subtitles MPSub-subtekstoj subtítulos MPSub MPSub azpitituluak MPSub-tekstitykset MPSub undirtekstir sous-titres MPSub fotheidil MPSub subtítulos MPSub כתוביות MPSub MPSub titlovi MPSub feliratok Subtitulos MPSub Subjudul MPSub Sottotitoli MPSub MPSub サブタイトル MPSub ქვეტიტრები MPSub субтитрлары MPSub 자막 파일 MPSub subtitrai MPSub subtitri MPSub undertekst MPSub-ondertitels MPSub-undertekstar sostítols MPSub Napisy MPSub legendas MPSub Legendas MPSub Subtitrări MPSub субтитры MPSub Titulky MPSub Datoteka podnapisov MPSub Nëntituj MPSub МПСуб преводи MPSub-undertexter MPSub altyazıları субтитри MPSub Phụ đề MPSub MPSub 字幕 MPSub 字幕 MPSub MPlayer Subtitle SSA subtitles ترجمات SSA Subtytry SSA Субтитри — SSA subtítols SSA titulky SSA SSA-undertekster SSA-Untertitel Υπότιτλοι SSA SSA subtitles SSA-subtekstoj subtítulos SSA SSA azpitituluak SSA-tekstitykset SSA undirtekstir sous-titres SSA fotheidil SSA Subtitulos SSA כתובית SSA SSA titlovi SSA feliratok Subtitulos SSA Subjudul SSA Sottotitoli SSA SSA 字幕 SSA субтитрлары SSA 자막 파일 SSA subtitrai SSA subtitri SSA undertekst SSA-ondertitels SSA-teksting sostítols SSA Napisy SSA legendas SSA Legendas SSA Subtitrări SSA субтитры SSA Titulky SSA Datoteka podnapisov SSA Nëntituj SSA ССА преводи SSA-undertexter SSA altyazıları субтитри SSA Phụ đề SSA SSA 字幕 SSA 字幕 SSA SubStation Alpha SubViewer subtitles ترجمات SubViewer Subtytry SubViewer Субтитри — SubViewer subtítols SubViewer titulky SubViewer SubViewer-undertekster SubViewer-Untertitel Υπότιτλοι SubViewer SubViewer subtitles SubViewer-subtekstoj subtítulos SubViewer SubViewer azpitituluak SubViewer-tekstitykset SubViewer undirtekstir sous-titres SubViewer fotheidil SubViewer subtítulos SubViewer כתוביות של SubViewer SubViewer titlovi SubViewer feliratok Subtitulos SubViewer Subjudul SubViewer Sottotitoli SubViewer SubViewer 字幕 SubViewer субтитрлары SubViewer 자막 파일 SubViewer subtitrai SubViewer subtitri SubViewer undertekst SubViewer-ondertitels SubViewer-teksting sostítols SubViewer Napisy SubViewer legendas SubViewer Legendas SubViewer Subtitrare SubViewer субтитры SubViewer Titulky SubViewer Datoteka podnapisov SubViewer Nëntituj SubViewer Суб Вјивер преводи SubViewer-undertexter SubViewer altyazıları субтитри SubViewer Phụ đề SubViewer SubViewer 字幕 SubViewer 字幕 iMelody ringtone نغمة iMelody Rington iMelody Аудио — iMelody to de trucada iMelody vyzváněcí melodie iMelody iMelody-ringetone iMelody-Klingelton ringtone iMelody iMelody ringtone tono de llamada iMelody iMelody doinua iMelody-soittoääni iMelody ringitóni sonnerie iMelody ton buailte iMelody Melodía de iMelody צלצול של iMelody iMelody ton zvonjenja iMelody csengőhang Tono de appello iMelody nada dering iMelody Suoneria iMelody iMelody リングトーン iMelody әуені iMelody 벨소리 iMelody skambučio melodija iMelody melodija iMelody ringetone iMelody-beltoon iMelody-ringetone sonariá iMelody Dzwonek iMelody toque iMelody Toque de celular do iMelody Sonerie iMelody мелодия iMelody Vyzváňacie melódie iMelody Zvonjenje iMelody Zile iMelody звоно ајМелодије iMelody-ringsignal iMelody melodisi рінгтон iMelody tiếng réo iMelody iMelody 铃声 iMelody 鈴聲 SMAF audio SMAF سمعي Aŭdyjo SMAF Аудио — SMAF àudio SMAF zvuk SMAF SMAF-lyd SMAF-Audio Ήχος SMAF SMAF audio SMAF-sondosiero sonido SMAF SMAF audioa SMAF-ääni SMAF ljóður audio SMAF fuaim SMAF son SMAF שמע SMAF SMAF audio SMAF hang Audio SMAF Audio SMAF Audio SMAF SMAF オーディオ SMAF аудиосы SMAF 오디오 SMAF garso įrašas SMAF audio SMAF-lyd SMAF-audio SMAF-lyd àudio SMAF Plik dźwiękowy SMAF áudio SMAF Áudio SMAF Audio SMAF аудио SMAF Zvuk SMAF Zvočna datoteka SMAF Audio SMAF СМАФ звук SMAF-ljud SMAF sesi звук SMAF Âm thanh SMAF SMAF 音频 SMAF 音訊 SMAF Synthetic music Mobile Application Format MRML playlist قائمة تشغيل MRML Śpis piesień MRML Списък за изпълнение — MRML llista de reproducció MRML seznam k přehrání MRML MRML-afspilningsliste MRML-Wiedergabeliste Λίστα αναπαραγωγής MRML MRML playlist MRML-ludlisto lista de reproducción MRML MRML erreprodukzio-zerrenda MRML-soittolista MRML avspælingarlisti liste de lecture MRML seinmliosta MRML lista de reprodución MRML רשימת השמעה MRML MRML popis za reprodukciju MRML-lejátszólista Lista de selection MRML Senarai putar MRML Playlist MRML MRML 再生リスト MRML რეპერტუარი MRML ойнау тізімі MRML 재생 목록 MRML grojaraštis MRML repertuārs MRML-spilleliste MRML-afspeellijst MRML-speleliste lista de lectura MRML Lista odtwarzania MRML lista de reprodução MRML Lista de reprodução do MRML Listă redare MRML список воспроизведения MRML Zoznam skladieb MRML Seznam predvajanja MRML Listë titujsh MRML МРМЛ списак нумера MRML-spellista MRML oynatma listesi список відтворення MRML Danh mục nhạc MRML MRML 播放列表 MRML 播放清單 MRML Multimedia Retrieval Markup Language XMF audio XMF سمعي Aŭdyjo XMF Аудио — XMF àudio XMF zvuk XMF XMF-lyd XMF-Audio Ήχος XMF XMF audio XMF-sondosiero sonido XMF XMF audioa XMF-ääni XMF ljóður audio XMF fuaim XMF son XMF שמע XMF XMF audio XMF hang Audio XMF Audio XMF Audio XMF XMF オーディオ XMF аудиосы XMF 오디오 XMF garso įrašas XMF audio XMF-lyd XMF-audio XMF-lyd àudio XMF Plik dźwiękowy XMF aúdio XMF Áudio XMF Audio XMF аудио XMF Zvuk XMF Zvočna datoteka XMF Audio XMF ИксМФ звук XMF-ljud XMF sesi звук XMF Âm thanh XMF XMF 音频 XMF 音訊 XMF eXtensible Music Format SV4 CPIO archive أرشيف SV4 CPIO SV4 CPIO arxivi Archiŭ SV4 CPIO Архив — SV4 CPIO arxiu CPIO SV4 archiv SV4 CPIO Archif CPIO SV4 SV4 CPIO-arkiv SV4-CPIO-Archiv Συμπιεσμένο αρχείο SV4 CPIO SV4 CPIO archive SV4-CPIO-arkivo archivador SV4 CPIO SV4 CPIO artxiboa SV4 CPIO -arkisto SV4 CPIO skjalasavn archive SV4 CPIO cartlann SV4 CPIO arquivo SV4 CPIO ארכיון של SV4 SPIO SV4 CPIO arhiva SV4 CPIO-archívum Archivo CPIO SV4 Arsip SV4 CPIO Archivio SV4 CPIO SV4 CPIO アーカイブ SV4 CPIO архиві SV4 CPIO 묶음 파일 SV4 CPIO archyvas SV4 CPIO arhīvs Arkib CPIO SV4 SV4 CPIO-arkiv SV4 CPIO-archief SV4 CPIO-arkiv archiu SV4 CPIO Archiwum SV4 CPIO arquivo SV4 CPIO Pacote SV4 CPIO Arhivă SV4 CPIO архив SV4 CPIO Archív SV4 CPIO Datoteka arhiva SV4 CPIO Arkiv SV4 CPIO СВ4 ЦПИО архива SV4 CPIO-arkiv SV4 CPIO arşivi архів SV4 CPIO Kho nén CPIO SV4 SV4 CPIO 归档文件 SV4 CPIO 封存檔 SV4 CPIO archive (with CRC) أرشيف SV4 CPIO (مع CRC) Archiŭ SV4 CPIO (z CRC) Архив — SV4 CPIO, проверка за грешки CRC arxiu CPIO SV4 (amb CRC) archiv SV4 CPIO (s CRC) SV4 CPIO-arkiv (med CRC) SV4-CPIO-Archiv (mit CRC) Συμπιεσμένο αρχείο SV4 CPIO (με CRC) SV4 CPIO archive (with CRC) SV4-CPIO-arkivo (kun CRC) archivador SV4 CPIO (con CRC) SV4 CPIO artxiboa (CRC-rekin) SV4 CPIO -arkisto (CRC:llä) SV4 CPIO skjalasavn (við CRC) archive SV4 CPIO (avec CRC) cartlann SV4 CPIO (le CRC) Arquivador SV4 CPIO (con CRC) ארכיון של SV4 SPIO (עם CRC) SV4 CPIO arhiva (s CRC-om) SV4 CPIO-archívum (CRC-vel) Archivo CPIO SV4 (con CRC) Arsip SV4 CPIO (dengan CRC) Archivio SV4 CPIO (con CRC) SV4 CPIO アーカイブ (CRC 有り) SV4 CPIO архиві (CRC бар) SV4 CPIO 묶음 파일(CRC 포함) SV4 CPII archyvas (su CRC) SV4 CPIO arhīvs (ar CRC) Arkib CPIO SV4 (dengan CRC) SV4 CPIO-arkiv (med CRC) SV4 CPIO-archief (met CRC) SV4 CPIO arkiv (med CRC) archiu SV4 CPIO (avec CRC) Archiwum SV4 CPIO (z sumą kontrolną) arquivo SV4 CPIO (com CRC) Pacote SV4 CPIO (com CRC) Arhivă SV4 CPIO (cu CRC) архив SV4 CPIP (с CRC) Archív SV4 CPIO (s CRC) Datoteka arhiva SV4 CPIO (z razpršilom CRC) Arkiv SV4 CPIO (me CRC) СВ4 ЦПИО архива (са ЦРЦ-ом) SV4 CPIO-arkiv (med CRC) SV4 CPIO arşivi (CRC ile) архів SV4 CPIO (з CRC) Kho nén CPIO SV4 (với CRC) SV4 CPIP 归档文件(带有 CRC) SV4 CPIO 封存檔 (具有 CRC) Tar archive أرشيف Tar Tar arxivi Archiŭ tar Архив — tar arxiu tar archiv Tar Archif tar Tar-arkiv Tar-Archiv Συμπιεσμένο αρχείο Tar Tar archive archivador Tar Tar artxiboa Tar-arkisto Tar skjalasavn archive tar cartlann Tar arquivo Tar ארכיון Tar Tar arhiva Tar archívum Archivo Tar Arsip Tar Archivio tar Tar アーカイブ Tar архиві TAR 묶음 파일 Tar archyvas Tar arhīvs Arkib Tar Tar-arkiv Tar-archief Tar-arkiv archiu tar Archiwum tar arquivo Tar Pacote Tar Arhivă Tar архив TAR Archív tar Datoteka arhiva Tar Arkiv tar Тар архива Tar-arkiv Tar arşivi архів tar Kho nén tar Tar 归档文件 Tar 封存檔 Tar archive (compressed) أرشيف Tar (مضغوط) Archiŭ tar (skampresavany) Архив — tar, компресиран arxiu tar (amb compressió) archiv Tar (komprimovaný) Tar-arkiv (komprimeret) Tar-Archiv (komprimiert) Αρχείο Tar (συμπιεσμένο) Tar archive (compressed) archivador Tar (comprimido) Tar artxiboa (konprimitua) Tar-arkisto (pakattu) Tar skjalasavn (stappað) archive tar (compressée) cartlann Tar (comhbhrúite) arquivo Tar (comprimido) ארכיון Tar (מכווץ) Tar arhiva (komprimirana) Tar archívum (tömörített) Archivo Tar (comprimite) Arsip Tar (terkompresi) Archivio tar (compresso) Tar アーカイブ (compress 圧縮) Tar архиві (сығылған) TAR 묶음 파일(압축) Tar archyvas (suglaudintas) Tar arhīvs (saspiests) Tar-arkiv (komprimert) Tar-archief (ingepakt) Tar-arkiv (pakka) archiu tar (compressat) Archiwum tar (skompresowane) arquivo Tar (comprimido) Pacote Tar (compactado) Arhivă Tar (comprimată) архив TAR (сжатый) Archív tar (komprimovaný) Datoteka arhiva Tar (stisnjen) Arkiv tar (i kompresuar) Тар архива (запакована) Tar-arkiv (komprimerat) Tar arşivi (sıkıştırılmış) архів tar (стиснений) Kho nén tar (đã nén) Tar 归档文件(压缩) Tar 封存檔 (UNIX 格式壓縮) generic font file ملف الخط العام zvyčajny fajł šryftu Шрифт fitxer genèric de tipus de lletra obecný soubor písma general skrifttypefil Allgemeine Schriftdatei Γενικό αρχείο γραμματοσειράς generic font file genera tipara dosiero tipo de letra genérico letra-tipo orokorra yleinen fonttitiedosto felagsstavasniðsfíla fichier de polices générique comhad cló ginearálta ficheiro de tipo de fonte xenérica קובץ גופן גנרי općenita datoteka fonta általános betűkészletfájl File de typo de litteras generic berkas fonta generik File tipo carattere generico 一般フォントファイル қаріп файлы 일반 글꼴 파일 bendras šrifto failas vispārēja fonta datne Fail font generik vanlig skriftfil algemeen lettertypebestand vanleg skrifttypefil fichièr de poliças generic Zwykły plik czcionki ficheiro genérico de letra Arquivo de fonte genérico fișier de font generic обычный файл шрифта Obyčajný súbor písma izvorna datoteka pisave File lloj gërme i përgjithshëm општа датотека слова allmän typsnittsfil genel yazı tipi dosyası загальний файл шрифту tập tin phông giống loài 通用字体文件 通用字型檔 packed font file ملف الخط المرزم zapakavany fajł šryftu Шрифт — компресиран fitxer empaquetat de tipus de lletra komprimovaný soubor písma pakket skrifttypefil Gepackte Schriftdatei Αρχείο συμπιεσμένης γραμματοσειράς packed font file pakigita tipara dosiero tipo de letra empaquetado Letra-tipo fitxategi paketatua pakattu fonttitiedosto pakkað stavasniðsfíla fichier de polices empaquetées comhad cló pacáilte ficheiro de fonte empaquetada קובץ גופן ארוז pakirana datoteka fonta packed font-fájl File de typos de litteras impacchettate berkas fonta terkemas File tipo carattere condensato パックされたフォントファイル қаріп файлы (дестеленген) 글꼴 묶음 파일 supakuotas šrifto failas sapakota fonta datne Fail font dipek pakket skriftfil ingepakt lettertypebestand pakka skrifttypefil fichièr de poliças empaquetadas Plik ze spakowaną czcionką ficheiro de letras empacotadas Arquivo de fonte empacotado fișier font împachetat сжатый файл шрифта Komprimovaný súbor písma pakirana datoteka pisave File lloj gërmash i kondensuar пакована датотека слова packad typsnittsfil paketlenmiş yazı tipi dosyası запакований файл шрифту tập tin phông chữ đã đóng gói 打包的字体文件 包裝字型檔 TGIF document مستند TGIF Dakument TGIF Документ — TGIF document TGIF dokument TGIF TGIF-dokument TGIF-Dokument Έγγραφο TGIF TGIF document TGIF-dokumento documento TGIF TGIF dokumentua TGIF-asiakirja TGIF skjal document TGIF cáipéis TGIF documento TGIF מסמך TGIF TGIF dokument TGIF-dokumentum Documento TGIF Dokumen TGIF Documento TGIF TGIF ドキュメント TGIF құжаты TGIF 문서 TGIF dokumentas TGIF dokuments Dokumen TGIF TGIF-dokument TGIF-document TGIF-dokument document TGIF Dokument TGIF documento TGIF Documento TGIF Document TGIF документ TGIF Dokument TGIF Dokument TGIF Dokument TGIF ТГИФ документ TGIF-dokument TGIF belgesi документ TGIF Tài liệu TGIF TGIF 文档 TGIF 文件 theme سمة örtük matyŭ Тема tema motiv thema tema Thema Θέμα theme etoso tema gaia teema tema thème téama tema ערכת נושא tema téma Thema tema Tema テーマ თემა тема 테마 tema motīvs Tema tema thema drakt tèma Motyw tema Tema temă тема Motív tema Temë тема tema tema тема sắc thái 主题 佈景主題 ToutDoux document مستند ToutDoux ToutDoux sənədi Dakument ToutDoux Документ — ToutDoux document ToutDoux dokument ToutDoux Dogfen ToutDoux ToutDoux-dokument ToutDoux-Dokument Έγγραφο ToutDoux ToutDoux document ToutDoux-dokumento documento de ToutDoux ToutDoux dokumentua ToutDoux-asiakirja ToutDoux skjal document ToutDoux cáipéis ToutDoux documento de ToutDoux מסמך של ToutDoux ToutDoux dokument ToutDoux-dokumentum Documento ToutDoux Dokumen ToutDoux Documento ToutDoux ToutDoux ドキュメント ToutDoux құжаты ToutDoux 문서 ToutDoux dokumentas ToutDoux dokuments Dokumen ToutDoux ToutDoux-dokument ToutDoux-document ToutDoux-dokument document ToutDoux Dokument ToutDoux documento ToutDoux Documento do ToutDoux Document ToutDoux документ ToutDoux Dokument ToutDoux Dokument ToutDoux Dokument ToutDoux Туду документ ToutDoux-dokument ToutDoux belgesi документ ToutDoux Tài liệu ToutDoux ToutDoux 文档 ToutDoux 文件 backup file ملف النسخ الاحتياطي zapasny fajł Резервно копие fitxer de còpia de seguretat záložní soubor sikkerhedskopi Sicherungsdatei Αντίγραφο ασφαλείας backup file restaŭrkopio archivo de respaldo babes-kopiako fitxategia varmuuskopio trygdarritsfíla fichier de sauvegarde comhad cúltaca ficheiro de copia de seguridade קובץ גיבוי Datoteka sigurnosne kopije biztonsági mentés Copia de reserva berkas cadangan File di backup バックアップファイル резервті көшірмесі 백업 파일 atsarginis failas dublējuma datne Fail backup sikkerhetskopi reservekopiebestand tryggleikskopi fichièr de salvament Plik zapasowy cópia de segurança Arquivo de backup fișier de backup резервная копия Záložný súbor varnostna kopija datoteke File backup датотека резерве säkerhetskopia yedek dosyası резервна копія tập tin sao lưu 备份文件 備份檔 Troff document مستند Troff Troff sənədi Dakument Troff Документ — Troff document Troff dokument Troff Dogfen troff Troffdokument Troff-Dokument Έγγραφο troff Troff document Troff-dokumento documento de Troff Troff dokumentua Troff-asiakirja Troff skjal document Troff cáipéis Troff documento Troff מסמך Troff Troff dokument Troff-dokumentum Documento Troff Dokumen Troff Documento Troff Troff 入力ドキュメント Troff құжаты Troff 문서 Troff dokumentas Troff dokuments Dokumen Troff Troff-dokument Troff-document Troff-dokument document Troff Dokument Troff documento Troff Documento Troff Document Troff документ Troff Dokument troff Dokument Troff Dokument Troff Трофф документ Troff-dokument Troff belgesi документ Troff Tài liệu Troff Troff 文档 Troff 文件 Manpage manual document document de pàgina man manuálová stránka Manpage-manualdokument Manpage-Handbuchdokument Έγγραφο βοήθειας manpage Manpage manual document documento de manual de Manpage Manpage dokument priručnika Manpage kézikönyv-dokumentum Pagina de manual "man" Dokumen manual manpage Documento di manuale manpage Manpage нұсқаулық құжаты 맨 페이지 설명서 문서 document de manual Manpage Dokument podręcznika stron pomocy documento de ajuda Manpage Documento Manpage документ справочной системы Manpage Dokument manuálu Manpage документ упутства странице упутства Manpage-manualdokument Man sayfası el kitabı belgesi документ підручника man Manpage 手册文档 Manpage 手冊說明文件 manual page (compressed) صفحة المساعدة (مضغوطة) man səhifəsi (sıxışdırılmış) staronka dapamohi (skampresavanaja) Страница от справочника, компресирана pàgina de manual (amb compressió) manuálová stránka (komprimovaná) tudalen llawlyfr (wedi ei gywasgu) manualside (komprimeret) Handbuchseite (komprimiert) Σελίδα οδηγιών (συμπιεσμένη) manual page (compressed) manpaĝo (kunpremita) página de manual (comprimida) eskuliburu orria (konprimitua) manuaalisivu (pakattu) handbókasíða (stappað) page de manuel (compressée) leathanach lámhleabhair (comhbhrúite) páxina de manual (comprimida) דף עזר (מכווץ) stranica priručnika (komprimirana) kézikönyvoldal (tömörített) Pagina de manual (comprimite) halaman manual (terkompresi) Pagina di manuale (compressa) (圧縮) man ページ әдістемелік парағы (сығылған) man 페이지(압축) žinyno puslapis (suglaudintas) rokasgrāmatas lapa (saspiesta) Halaman manual (termampat) manualside (komprimert) handleidingspagina (ingepakt) manualside (pakka) pagina de manual (compressat) Strona podręcznika (skompresowana) página de manual (comprimida) Página de manual (compactada) pagină de manual (comprimată) страница руководства (сжатая) Manuálová stránka (komprimovaná) stran priročnika (stisnjena) Faqe manuali (e kompresuar) страница упутства (запакована) manualsida (komprimerad) kılavuz dosyası (sıkıştırılmış) сторінка посібника (стиснена) trang hướng dẫn (đã nén) 手册页 (压缩) 手冊頁面 (壓縮版) Tar archive (LZO-compressed) أرشيف Tar (مضغوط-LZO) Archiŭ tar (LZO-skampresavany) Архив — tar, компресиран с LZO arxiu tar (amb compressió LZO) archiv Tar (komprimovaný pomocí LZO) Tar-arkiv (LZO-komprimeret) Tar-Archiv (LZO-komprimiert) Αρχείο Tar (συμπιεσμένο με LZO) Tar archive (LZO-compressed) archivador Tar (comprimido con LZO) Tar artxiboa (LZO-rekin konprimitua) Tar-arkisto (LZO-pakattu) Tar skjalasavn (LZO-stappað) archive tar (compression LZO) cartlann Tar (comhbhrúite le LZO) arquivo Tar (comprimido con LZO) ארכיון Tar (מכווץ ע״י LZO) Tar arhiva (komprimirana LZO-om) Tar archívum (LZO-val tömörítve) Archivo Tar (comprimite con LZO) Arsip Tar (terkompresi LZO) Archivio tar (compresso con LZO) Tar アーカイブ (LZO 圧縮) Tar архиві (LZO-мен сығылған) TAR 묶음 파일(LZO 압축) Tar archyvas (suglaudintas su LZO) Tar arhīvs (saspiests ar LZO) Tar-arkiv (LZO-komprimert) Tar-archief (ingepakt met LZO) Tar-arkiv (pakka med LZO) archiu tar (compression LZO) Archiwum tar (kompresja LZO) arquivo Tar (compressão LZO) Pacote Tar (compactado com LZO) Arhivă Tar (comprimată LZO) архив TAR (сжатый LZO) Archív tar (komprimovaný pomocou LZO) Datoteka arhiva Tar (stisnjen z LZO) Arkiv tar (i kompresuar me LZO) Тар архива (запакована ЛЗО-ом) Tar-arkiv (LZO-komprimerat) Tar arşivi (LZO ile sıkıştırılmış) архів tar (стиснений LZO) Kho nén tar (đã nén LZO) Tar 归档文件(LZO 压缩) Tar 封存檔 (LZO 格式壓縮) XZ archive أرشيف XZ Архив — XZ arxiu XZ archiv XZ XZ-arkiv XZ-Archiv Συμπιεσμένο αρχείο XZ XZ archive XZ-arkivo archivador XZ XZ artxiboa XZ-arkisto XZ skjalasavn archive XZ cartlann XZ ficheiro XZ ארכיון XZ XZ arhiva XZ-archívum Archivo XZ Arsip XZ Archivio xz XZ アーカイブ XZ архиві XZ 압축 파일 XZ archyvas XZ arhīvs XZ archief archiu XZ Archiwum XZ arquivo XZ Pacote XZ Arhivă XZ архив XZ Archív XZ Datoteka arhiva XZ ИксЗ архива XZ-arkiv XZ arşivi архів XZ XZ 归档文件 XZ 封存檔 Tar archive (XZ-compressed) أرشيف Tar (مضغوط-XZ) Архив — tar, компресиран с XZ arxiu tar (amb compressió XZ) archiv Tar (komprimovaný pomocí XZ) Tar-arkiv (XZ-komprimeret) Tar-Archiv (XZ-komprimiert) Αρχείο Tar (συμπιεσμένο με XZ) Tar archive (XZ-compressed) archivador Tar (comprimido con XZ) Tar artxiboa (XZ-rekin konprimitua) Tar-arkisto (XZ-pakattu) Tar skjalasavn(XZ-stappað) archive tar (compression XZ) cartlann Tar (comhbhrúite le XZ) arquivo Tar (comprimido con XZ) ארכיון Tar (מכווץ ע״י XZ) Tar arhiva (komprimirana XZ-om) Tar archívum (XZ-vel tömörítve) Archivo Tar (comprimite con XZ) Arsip Tar (terkompresi XZ) Archivio tar (compresso con XZ) Tar アーカイブ (XZ 圧縮) Tar архиві (XZ-мен сығылған) TAR 묶음 파일(XZ 압축) Tar archyvas (suglaudintas su XZ) Tar arhīvs (saspiests ar XZ) Tar archief (XZ-compressed) archiu tar (compression XZ) Archiwum tar (kompresja XZ) arquivo Tar (compressão XZ) Pacote Tar (compactado com XZ) Arhivă Tar (comprimată XZ) архив TAR (сжатый XZ) Archív tar (komprimovaný pomocou XZ) Datoteka arhiva Tar (stisnjen z XZ) Тар архива (запакована ИксЗ-ом) Tar-arkiv (XZ-komprimerat) Tar arşivi (XZ ile sıkıştırılmış) архів tar (стиснений XZ) Tar 归档文件(XZ 压缩) Tar 封存檔 (XZ 格式壓縮) PDF document (XZ-compressed) Документ — PDF, компресиран с XZ document PDF (amb compressió XZ) dokument PDF (komprimovaný pomocí XZ) PDF-dokument (XZ-komprimeret) PDF-Dokument (XZ-komprimiert) Έγγραφο PDF (συμπιεσμένο με XZ) PDF document (XZ-compressed) documento PDF (comprimido con XZ) PDF dokumentua (XZ-rekin konprimitua) PDF-asiakirja (XZ-pakattu) document PDF (compressé XZ) documento PDF (comprimido en XZ) מסמך PDF (מכווץ ע״י XZ) PDF dokument (komprimiran XZ-om) PDF dokumentum (XZ-vel tömörített) Documento PDF (comprimite con XZ) Dokumen PDF (terkompresi XZ) Documento PDF (compresso con XZ) PDF 文書(XZ 圧縮) PDF დოკუმენტი (XZ-ით შეკუმშული) PDF құжаты (XZ-мен сығылған) PDF 문서(XZ 압축) PDF dokuments (saspiests ar XZ) PDF document (XZ-compressed) document PDF (compressat XZ) Dokument PDF (kompresja XZ) documento PDF (compressão XZ) Documento PDF (compactado com XZ) документ PDF (сжатый XZ) Dokument PDF (komprimovaný pomocou XZ) Dokument PDF (XZ-stisnjen) ПДФ документ (запакован ИксЗ-ом) PDF-dokument (XZ-komprimerat) PDF belgesi (XZ ile sıkıştırılmış) документ PDF (стиснений xz) PDF 文档(XZ) PDF 文件 (XZ 格式壓縮) Ustar archive أرشيف Ustar Archiŭ ustar Архив — ustar arxiu ustar archiv Ustar Ustararkiv Ustar-Archiv Συμπιεσμένο αρχείο Ustar Ustar archive Ustar-arkivo archivador de Ustar Ustar artxiboa Ustar-arkisto Ustar skjalasavn archive Ustar cartlann Ustar arquivo Ustar ארכיון Ustar Ustar arhiva Ustar archívum Archivo Ustar Arsip Ustar Archivio ustar Ustar アーカイブ Ustar архиві Ustar 압축 파일 Ustar archyvas Ustar arhīvs Ustar-arkiv Ustar-archief Ustar-arkiv archiu Ustar Archiwum ustar arquivo Ustar Pacote Ustar Arhivă Ustar архив Ustar Archív ustar Datoteka arhiva Ustar Arkiv Ustar Устар архива Ustar-arkiv Ustar arşivi архів ustar Kho nén ustar Ustar 归档文件 Ustar 封存檔 WAIS source code شفرة مصدر WAIS WAIS mənbə faylı Kryničny kod WAIS Изходен код — WAIS codi font en WAIS zdrojový kód WAIS Ffynhonnell Rhaglen WAIS WAIS-kildekode WAIS-Quelltext Πηγαίος κώδικας WAIS WAIS source code WAIS-fontkodo código fuente en WAIS WAIS iturburu-kodea WAIS-lähdekoodi WAIS keldukota code source WAIS cód foinseach WAIS código fonte WAIS קוד מקור של WAIS WAIS izvorni kod WAIS-forráskód Codice-fonte WAIS Kode program WAIS Codice sorgente WAIS WAIS ソースコード WAIS бастапқы коды WAIS 소스 코드 WAIS pradinis kodas WAIS pirmkods Kod sumber WAIS WAIS-kildekode WAIS-broncode WAIS-kjeldekode còde font WAIS Plik źródłowy WAIS código origem WAIS Código-fonte WAIS Cod sursă WAIS исходный код WAIS Zdrojový kód WAIS Datoteka izvorne kode WAIS Kod burues WAIS ВАИС изворни ко̂д WAIS-källkod WAIS kaynak kodu вихідний код мовою WAIS Mã nguồn WAIS WAIS 源代码 WAIS 源碼 WordPerfect/Drawperfect image صورة WordPerfect/Drawperfect Vyjava WordPerfect/Drawperfect Изображение — WordPerfect/Drawperfect imatge de WordPerfect/Drawperfect obrázek WordPerfect/Drawperfect WordPerfect/Drawperfect-billede WordPerfect/DrawPerfect-Bild Εικόνα WordPerfect/Drawperfect WordPerfect/Drawperfect image WordPerfect/Drawperfect-bildo imagen de WordPerfect/Drawperfect WordPerfect/Drawperfect irudia WordPerfect/Drawperfect-kuva WordPerfect/Drawperfect mynd image WordPerfect/DrawPerfect íomhá WordPerfect/Drawperfect imaxe de WordPerfect/DrawPerfect תמונה של WordPerfect/Drawperfect WordPerfect/Drawperfect slika WordPerfect/Drawperfect-kép Imagine WordPerfect/DrawPerfect Gambar WordPerfect/Drawperfect Immagine WordPerfect/Drawperfect WordPerfect/Drawperfect 画像 WordPerfect/Drawperfect суреті WordPerfect/Drawperfect 그림 WordPerfect/Drawperfect paveikslėlis WordPerfect/Drawperfect attēls Imej WordPerfect/Drawperfect WordPerfect-/Drawperfect-bilde WordPerfect/Drawperfect-afbeelding WordPerfect/DrawPerfect-bilete imatge WordPerfect/DrawPerfect Obraz WordPerfect/DrawPerfect imagem do WordPerfect/Drawperfect Imagem do WordPerfect/Drawperfect Imagine WordPerfect/Drawperfect изображение WordPerfect/Drawperfect Obrázok WordPerfect/Drawperfect Slikovna datoteka Drawperfect Figurë WordPerfect/Drawperfect Ворд Перфект/Дров Перфект слика WordPerfect/Drawperfect-bild WordPerfect/DrawPerfect görüntüsü зображення WordPerfect/Drawperfect Ảnh WordPerfect/Drawperfect WordPerfect/Drawperfect 图像 WordPerfect/Drawperfect 影像 DER/PEM/Netscape-encoded X.509 certificate شهادة DER/PEM/Netscape-encoded X.509 Sertyfikat X.509, zakadavany ŭ DER/PEM/Netscape Сертификат — DER/PEM/Netscape X.509 certificat X.509 codificat com DER/PEM/Netscape certifikát X.509 kódovaný jako DER/PEM/Netscape DER-/PEM-/Netscapekodet X.509-certifikat DER/PEM/Netscape-kodiertes X.509-Zertifikat Ψηφιακό πιστοποιητικό X.509 κωδικοποιημένο κατά DER/PEM/Netscape DER/PEM/Netscape-encoded X.509 certificate DER/PEM/Netscape-kodigita X.509-atestilo certificado X.509 codificado con DER/PEM/Netscape X.509rekin kodetutako DER, PEM edo Netscape zertifikatua DER/PEM/Netscape-koodattu X.509-varmenne DER/PEM/Netscape-encoded X.509 váttan certificat X.509 codé DER/PEM/Netscape teastas X.509 ionchódaithe le DER/PEM/Netscape certificado X.509 codificado con DER/PEM/Netscape אישור מסוג X.509 של DER/PEM/Netscape-encoded DER/PEM/Netscape-kodiran X.509 certifikat DER/PEM/Netscape formátumú X.509-tanúsítvány Certificato X.509 codificate in DER/PEM/Netscape Sertifikat DER/PEM/Netscape-tersandi X.509 Certificato X.509 DER/PEM/Netscape DER/PEM/Netscape エンコード X.509 証明書 DER/PEM/Netscape კოდირებული X.509 სერტიფიკატი X.509 сертификаты (DER/PEM/Netscape кодталған) DER/PEM/넷스케이프로 인코딩된 X.509 인증서 DER/PEM/Netscape-encoded X.509 liudijimas DER/PEM/Netscape-encoded X.509 sertifikāts Sijil X.509 dienkod /DER/PEM/Netscape DER/PEM/Netscape-kodet X.509-sertifikat DER/PEM/Netscape-gecodeerd X.509-certificaat DER/PEM/Netscape-koda X.509-sertifikat certificat X.509 encodat DER/PEM/Netscape Zakodowany w DER/PEM/Netscape certyfikat X.509 certificado X.509 codificado com DER/PEM/Netscape Certificado X.509 codificado com DER/PEM/Netscape Certificat DER/PEM/Netscape-codat X.509 сертификат X.509 (DER/PEM/Netscape-закодированный) Certifikát X.509 kódovaný ako DER/PEM/Netscape Datoteka potrdila DER/PEM/Netscape X.509 Çertifikatë DER/PEM/Netscape-encoded X.509 ДЕР/ПЕМ/Нетскејп кодирано уверење Икс.509 DER/PEM/Netscape-kodat X.509-certifikat DER/PEM/Netscape-kodlanmış X.509 sertfikası сертифікат X.509 у форматі DER/PEM/Netscape Chứng nhận X.509 mã hoá bằng Netscape/PEM/DER DER/PEM/Netscape-encoded X.509 证书 DER/PEM/Netscape 編碼的 X.509 憑證 empty document مستند فارغ pusty dakument Празен документ document buit prázdný dokument tomt dokument Leeres Dokument Κενό έγγραφο empty document malplena dokumento documento vacío dokumentu hutsa tyhjä asiakirja tómt skjal document vide cáipéis fholamh documeto baleiro מסמך ריק prazan dokument üres dokumentum Documento vacue dokumen kosong Documento vuoto 空のドキュメント бос құжат 빈 문서 tuščias dokumentas tukšs dokuments Dokumen kosong tomt dokument leeg document tomt dokument document void Pusty dokument documento vazio Documento vazio document gol пустой документ Prázdny dokument prazen dokument Dokument bosh празан документ tomt dokument boş belge порожній документ tài liệu rỗng 空文档 空白文件 Zoo archive أرشيف Zoo Zoo arxivi Archiŭ zoo Архив — zoo arxiu zoo archiv Zoo Archif zoo Zooarkiv Zoo-Archiv Συμπιεσμένο αρχείο Zoo Zoo archive Zoo-arkivo archivador Zoo Zoo artxiboa Zoo-arkisto Zoo skjalasavn archive zoo cartlann Zoo ficheiro Zoo ארכיון Zoo Zoo arhiva Zoo archívum Archivo Zoo Arsip Zoo Archivio zoo Zoo アーカイブ Zoo архиві ZOO 압축 파일 Zoo archyvas Zoo arhīvs Zoo-arkiv Zoo-archief Zoo-arkiv archiu zoo Archiwum zoo arquivo Zoo Pacote Zoo Arhivă Zoo архив ZOO Archív zoo Datoteka arhiva ZOO Arkiv zoo Зoo архива Zoo-arkiv Zoo arşivi архів zoo Kho nén zoo Zoo 归档文件 Zoo 封存檔 XHTML page صفحة XHTML Staronka XHTML Страница — XHTML pàgina XHTML stránka XHTML XHTML-side XHTML-Seite Σελίδα XHTML XHTML page XHTML-paĝo página XHTML XHTML orria XHTML-sivu XHTML síða page XHTML leathanach XHTML Páxina XHTML דף XHTML XHTML stranica XHTML-oldal Pagina XHTML Halaman XHTML Pagina XHTML XHTML ページ XHTML парағы XHTML 페이지 XHTML puslapis XHTML lapa Laman XHTML XHTML-side XHTML-pagina XHTML-side pagina XHTML Strona XHTML página XHTML Página XHTML Pagină XHTML страница XHTML Stránka XHTML Datoteka spletne strani XHTML Faqe XHTML ИксХТМЛ страница XHTML-sida XHTML sayfası сторінка XHTML Trang XHTML XHTML 页面 XHTML 網頁 XHTML Extensible HyperText Markup Language Zip archive أرشيف Zip Zip arxivi Archiŭ zip Архив — zip arxiu zip archiv ZIP Archif ZIP Ziparkiv Zip-Archiv Συμπιεσμένο αρχείο Zip Zip archive Zip-arkivo archivador Zip Zip artxiboa Zip-arkisto Zip skjalasavn archive zip cartlann Zip ficheiro Zip ארכיון Zip Zip arhiva Zip archívum Archivo Zip Arsip Zip Archivio zip Zip アーカイブ Zip архиві ZIP 압축 파일 Zip archyvas Zip arhīvs Zip-arkiv Zip-archief Zip-arkiv archiu zip Archiwum ZIP arquivo Zip Pacote Zip Arhivă zip архив ZIP Archív ZIP Datoteka arhiva ZIP Arkiv zip Зип архива Zip-arkiv Zip arşivi архів zip Kho nén zip Zip 归档文件 Zip 封存檔 WIM disk Image imatge de disc WIM obraz disku WIM WIM-diskaftryk WIM-Datenträgerabbild Εικόνα δίσκου WIM WIM disk Image imagen de disco WIM WIM disko irudia WIM-levytiedosto WIM slika diska WIM lemezkép Imagine de disco WIM Image disk WIM Immagine disco WIM WIM диск бейнесі WIM 디스크 이미지 imatge disc WIM Obraz dysku WIM imagem de disco WIM Imagem de disco WIM образ диска WIM Obraz disku WIM слика диска ВИМ-а WIM-diskavbild WIM disk kalıbı образ диска WIM WIM 磁盘镜像 WIM 磁碟映像檔 WIM Windows Imaging Format Dolby Digital audio Dolby Digital سمعي Dolby Digital audio Aŭdyjo Dolby Digital Аудио — Dolby Digital àudio de Dolby Digital zvuk Dolby Digital Sain Dolby Digital Dolby Ditital-lyd Dolby-Digital-Audio Ψηφιακός Ήχος Dolby Dolby Digital audio Sondosiero en Dolby Digital sonido Dolby Digital Dolby audio digitala Dolby Digital -ääni Dolby Digital ljóður audio Dolby Digital fuaim Dolby Digital son Dolby Digital שמע Dolby Digital Dolby Digital audio Dolby Digital hang Audio Dolby Digital Audio Dolby Digital Audio Dolby Digital ドルビーデジタルオーディオ Dolby Digital-ის აუდიო Dolby Digital аудиосы 돌비 디지털 오디오 Dolby Digital garso įrašas Dolby Digital audio Audio Digital Dolby Dolby digital lyd Dolby Digital-audio Dolby Digital lyd àudio Dolby Digital Plik dźwiękowy Dolby Digital áudio Dolby Digital Áudio Dolby Digital Audio Dolby Digital аудио Dolby Digital Zvuk Dolby Digital Zvočna datoteka Dolby Digital Audio Dolby Digital Дигитални Долби звук Dolby Digital-ljud Dolby Digital sesi звук Dolby Digital Âm thanh Dolby Digital 杜比数字音频 杜比數位音訊 DTS audio àudio DTS zvuk DTS DTS-lyd DTS-Audio Ήχος DTS DTS audio sonido DTS DTS audioa DTS-ääni audio DTS Son DTS שמע DTS DTS zvučni zapis DTS hang Audio DTS Audio DTS Audio DTS DTS オーディオ DTS аудиосы DTS 오디오 DTS audio àudio DTS Dźwięk DTS aúdio DTS Áudio DTS аудио DTS Zvuk DTS Zvok DTS ДТС звук DTS-ljud DTS sesi звукові дані DTS DTS 音频 DTS 音訊 DTSHD audio àudio DTSHD zvuk DTSHD DTSDH-lyd DTSHD-Audio Ήχος DTSHD DTSHD audio sonido DTSHD DTSHD audioa DTS-HD-ääni audio DTSHD Son DTSHD שמע DTSHD DTSHD zvučni zapis DTSHD hang Audio DTSHD Audio DTSHD Audio DTSHD DTSHD オーディオ DTSHD аудиосы DTSHD 오디오 DTSHD audio àudio DTSHD Dźwięk DTSHD áudio DTSHD Áudio DTSHD аудио DTSHD Zvuk DTSHD Zvok DTSHD ДТСХД звук DTSHD-ljud DTSHD sesi звукові дані DTSHD DTSHD 音频 DTSHD 音訊 AMR audio AMR سمعي Aŭdyjo AMR Аудио — AMR àudio AMR zvuk AMR AMR-lyd AMR-Audio Ήχος AMR AMR audio AMR-sondosiero sonido AMR AMR audioa AMR-ääni AMR ljóður audio AMR fuaim AMR son AMR שמע AMR AMR audio AMR hang Audio AMR Audio AMR Audio AMR AMR オーディオ AMR აუდიო AMR аудиосы AMR 오디오 AMR garso įrašas AMR audio AMR-lyd AMR-audio AMR-lyd àudio AMR Plik dźwiękowy AMR áudio AMR Áudio AMR Audio AMR аудио AMR Zvuk AMR Zvočna datoteka AMR Audio AMR АМР звук AMR-ljud AMR sesi звук AMR Âm thanh AMR AMR 音频 AMR 音訊 AMR Adaptive Multi-Rate AMR-WB audio AMR-WB سمعي Aŭdyjo AMR-WB Аудио — AMR-WB àudio AMR-WB zvuk AMR-WB AMR-WB-lyd AMR-WB-Audio Ήχος AMR-WB AMR-WB audio AMR-WB-sondosiero sonido AMR-WB AMR-WB audioa AMR-WB-ääni AMR-WB ljóður audio AMR-WB fuaim AMR-WB son AMR-WB שמע AMR-WN AMR-WB audio AMR-WB hang Audio AMR-WB Audio AMR-WB Audio AMR-WB AMR-WB オーディオ AMR-WB აუდიო AMR-WB аудиосы AMR-WB 오디오 AMR-WB garso įrašas AMR-WB audio AMR-WB-lyd AMR-WB-audio AMR-WB-lyd àudio AMR-WB Plik dźwiękowy AMR-WB áudio AMR-WB Áudio AMR-WB Audio AMR-WB аудио AMR-WB Zvuk AMR-WB Zvočna datoteka AMR-WB Audio AMR-WB АМР-ВБ звук AMR-WB-ljud AMR-WB sesi звук AMR-WB Âm thanh AMR-WB AMR-WB 音频 AMR-WB 音訊 AMR-WB Adaptive Multi-Rate Wideband ULAW (Sun) audio ULAW (صن) سمعي ULAW (Sun) audio faylı Aŭdyjo ULAW (Sun) Аудио — ULAW, Sun àudio ULAW (Sun) zvuk ULAW (Sun) Sain ULAW (Sun) ULAW-lyd (Sun) ULAW-Audio (Sun) Ήχος ULAW (Sun) ULAW (Sun) audio ULAW-sondosiero (Sun) sonido ULAW (Sun) ULAW (sun) audioa ULAW (Sun) -ääni ULAW (Sun) ljóður audio ULAW (Sun) fuaim ULAW (Sun) son ULAW (Sun) שמע ULAW (של Sun) ULAW (Sun) zvučni zapis ULAW (Sun) hang Audio ULAW (sun) Audio ULAW (Sun) Audio ULAW (Sun) ULAW (Sun) オーディオ ULAW (Sun) аудиосы ULAW(Sun) 오디오 ULAW (Sun) garso įrašas ULAW (Sun) audio Audio ULAW (Sun) ULAW-lyd (Sun) (Sun) ULAW-audio ULAW (Sun)-lyd àudio ULAW (Sun) Plik dźwiękowy ULAW (Sun) áudio ULAW (Sun) Áudio ULAW (Sun) Fișier audio ULAW (Sun) аудио ULAW (Sun) Zvuk ULAW (Sun) Zvočna datoteka ULAW (Sun) Audio ULAW (Sun) УЛАВ (Сан) звук ULAW-ljud (Sun) ULAW (Sun) sesi звук ULAW (Sun) Âm thanh ULAW (Sun) ULAW (Sun) 音频 ULAW (Sun) 音訊 Commodore 64 audio Commodore 64 سمعي Aŭdyjo Commodore 64 Аудио — Commodore 64 àudio de Commodore 64 zvuk Commodore 64 Commodore 64-lyd Commodore-64-Audio Ήχος Commodore 64 Commodore 64 audio Sondosiero de Commodore 64 sonido de Commodore 64 Commodore 64 Audioa Commodore 64 -ääni Commodore 64 ljóð audio Commodore 64 fuaim Commodore 64 son de Commodore 64 שמע של Commodore 64 Commodore 64 audio Commodore 64 hang Audio Commodore 64 Audio Commodore 64 Audio Commodore 64 Commodore 64 オーディオ Commodore 64-ის აუდიო Commodore 64 аудиосы Commodore 64 오디오 Commodore 64 garso įrašas Commodore 64 audio Audio Commodore 64 Commodore 64-lyd Commodore 64-audio Commodore 64-lyd àudio Commodore 64 Plik dźwiękowy Commodore 64 áudio Commodore 64 Áudio Commodore 64 Audio Commodore 64 аудио Commodore 64 Zvuk Commodore 64 Zvočna datoteka Commodore 64 Audio Commodore 64 звук Комодора 64 Commodore 64-ljud Commodore 64 sesi звук Commodore 64 Âm thanh Commodore 64 Commodore 64 音频 Commodore 64 音訊 PCM audio سمعي PCM PCM audio faylı Aŭdyjo PCM Аудио — PCM àudio PCM zvuk PCM Sain PCM PCM-lyd PCM-Audio Ήχος PCM PCM audio PCM-sondosiero sonido PCM PCM audioa PCM-ääni PCM ljóður audio PCM fuaim PCM son PCM שמע PCM PCM zvučni zapis PCM hang Audio PCM Audio PCM Audio PCM PCM オーディオ PCM аудиосы PCM 오디오 PCM garso įrašas PCM audio Audio PCM PCM-lyd PCM-audio PCM-lyd àudio PCM Plik dźwiękowy PCM áudio PCM Áudio PCM Audio PCM аудио PCM Zvuk PCM Zvočna datoteka PCM Audio PCM ПЦМ звук PCM-ljud PCM sesi звук PCM Âm thanh PCM PCM 音频 PCM 音訊 AIFC audio AIFC سمعي AIFC audio faylı Aŭdyjo AIFC Аудио — AIFC àudio AIFC zvuk AIFC Sain AIFC AIFC-lyd AIFC-Audio Ήχος AIFC AIFC audio AIFC-sondosiero sonido AIFC AIFC audioa AIFC-ääni AIFC ljóður audio AIFC fuaim AIFC son AIFC שמע AIFC AIFC audio AIFC hang Audio AIFC Audio AIFC Audio AIFC AIFC オーディオ AIFC აუდიო AIFC аудиосы AIFC 오디오 AIFC garso įrašas AIFC audio Audio AIFC AIFC-lyd AIFC-audio AIFC-lyd àudio AIFC Plik dźwiękowy AIFC áudio AIFC Áudio AIFC Fișier audio AIFC аудио AIFC Zvuk AIFC Zvočna datoteka AIFC Audio AIFC АИФЦ звук AIFC-ljud AIFC sesi звук AIFC Âm thanh AIFC AIFC 音频 AIFC 音訊 AIFC Audio Interchange File format Compressed AIFF/Amiga/Mac audio AIFF/Amiga/Mac سمعي AIFF/Amiga/Mac audio faylı Aŭdyjo AIFF/Amiga/Mac Аудио — AIFF/Amiga/Mac àudio AIFF/Amiga/Mac zvuk AIFF/Amiga/Mac Sain AIFF/Amiga/Mac AIFF-/Amiga-/Maclyd AIFF/Amiga/Mac-Audio Ήχος AIFF/Amiga/Mac AIFF/Amiga/Mac audio AIFF/Amiga/Mac-sondosiero sonido AIFF/Amiga/Mac AIFF/Amiga/Mac audioa AIFF/Amiga/Mac-ääni AIFF/Amiga/Mac ljóður audio AIFF/Amiga/Mac fuaim AIFF/Amiga/Mac son AIFF/Amiga/Mac שמע AIFF/Amiga/Mac AIFF/Amiga/Mac audio AIFF/Amiga/Mac hang Audio AIFF/Amiga/Mac Audio AIFF/Amiga/Mac Audio AIFF/Amiga/Mac AIFF/Amiga/Mac オーディオ AIFF/Amiga/Mac აუდიო AIFF/Amiga/Mac аудиосы AIFF/Amiga/Mac 오디오 AIFF/Amiga/Mac garso įrašas AIFF/Amiga/Mac audio Audio AIFF/Amiga/Mac AIFF/Amiga/Mac-lyd AIFF/Amiga/Mac-audio AIFF/Amiga/Mac-lyd àudio AIFF/Amiga/Mac Plik dźwiękowy AIFF/Amiga/Mac áudio AIFF/Amiga/Mac Áudio AIFF/Amiga/Mac Audio AIFF/Amiga/Mac аудио AIFF/Amiga/Mac Zvuk AIFF/Amiga/Mac Zvočna datoteka AIFF/Amiga/Mac Audio AIFF/Amiga/Mac АИФФ/Амига/Мекинтош звук AIFF/Amiga/Mac-ljud AIFF/Amiga/Mac sesi звук AIFF/Amiga/Mac Âm thanh AIFF/Amiga/Mac AIFF/Amiga/Mac 音频 AIFF/Amiga/Mac 音訊 AIFF Audio Interchange File Format Monkey's audio Monkey سمعي Aŭdyjo Monkey's Аудио — Monkey àudio de Monkey zvuk Monkey's Monkeys lyd Monkey's-Audio Ήχος Monkey's Monkey's audio sonido de Monkey Monkey audioa Monkey's Audio -ääni Monkey's ljóður audio Monkey fuaim Monkey's son de Monkey שמע של Monkey's Monkey zvučni zapis Monkey hang Audio Monkey's Audio Monkey Audio Monkey's Monkey's オーディオ Monkey аудиосы Monkey's 오디오 Monkey garso įrašas Monkey's audio Monkey's-lyd Monkey's-audio Monkey's Audio-lyd àudio Monkey Plik dźwiękowy Monkey's Audio áudio Monkey Áudio Monkey's Audio Monkey's аудио Monkey's Zvuk Monkey's Zvočna datoteka Monkey Audio Monkey's Монкијев звук Monkey's audio Monkey's sesi звук Monkey's Âm thanh cua Monkey Monkey's audio 音频 Monkey's 音訊 Impulse Tracker audio Impulse Tracker سمعي Impulse Tracker audio faylı Aŭdyjo Impulse Tracker Аудио — Impulse Tracker àudio d'Impulse Tracker zvuk Impulse Tracker Sain Impulse Tracker Impulse Tracker-lyd Impulse-Tracker-Audio Ήχος Impulse Tracker Impulse Tracker audio Sondosiero de Impulse Tracker sonido de Impulse Tracker Impulse Tracker audioa Impulse Tracker -ääni Impulse Tracker ljóður audio Impulse Tracker fuaim Impulse Tracker son de Impulse Tracker שמע של Impulse Tracker Impulse Tracker audio Impulse Tracker hang Audio Impulse Tracker Audio Impulse Tracker Audio Impulse Tracker Impulse Tracker オーディオ Impulse Tracker аудиосы Impulse Tracker 오디오 Impulse Tracker garso įrašas Impulse Tracker audio Audio Impulse Tracker Impulse Tracker-lyd Impulse Tracker-audio Impulse Tracker lyd àudio Impulse Tracker Plik dźwiękowy Impulse Tracker áudio Impulse Tracker Áudio Impulse Tracker Audio Impulse Tracker аудио Impulse Tracker Zvuk Impulse Tracker Zvočna datoteka Impulse Tracker Audio Impulse Tracker звук Пратиоца Импулса Impulse Tracker-ljud Impulse Tracker sesi звук Impulse Tracker Âm thanh Impulse Tracker Impulse Tracker 音频 Impulse Tracker 音訊 FLAC audio FLAC سمعي Aŭdyjo FLAC Аудио — FLAC àudio FLAC zvuk FLAC FLAC-lyd FLAC-Audio Ήχος FLAC FLAC audio FLAC-sondosiero sonido FLAC FLAC audioa FLAC-ääni FLAC ljóður audio FLAC fuaim FLAC son FLAC קובץ שמע מסוג FLAC FLAC audio FLAC hang Audio FLAC Audio FLAC Audio FLAC FLAC オーディオ FLAC აუდიო FLAC аудиосы FLAC 오디오 FLAC garso įrašas FLAC audio Audio FLAC FLAC-lyd FLAC-audio FLAC-lyd àudio FLAC Plik dźwiękowy FLAC áudio FLAC Áudio FLAC Audio FLAC аудио FLAC Zvuk FLAC Zvočna datoteka Flac Audio FLAC ФЛАЦ звук FLAC-ljud FLAC sesi звук FLAC Âm thanh FLAC FLAC 音频 FLAC 音訊 WavPack audio WavPack سمعي Aŭdyjo WavPack Аудио — WavPack àudio de WavPack zvuk WavPack WavPack-lyd WavPack-Audio Ήχος WavePack WavPack audio WavPack-sondosiero sonido WavPack WavPack audioa WavPack-ääni WavPack ljóður audio WavPack fuaim WavPack son WavPack שמע WavPack WavPack audio WavPack hang Audio WavPack Audio WavPack Audio WavPack WavPack オーディオ WavPack аудиосы WavPack 오디오 WavPack garso įrašas WavPack audio WavPack-lyd WavPack-audio WavPack-lyd àudio WavPack Plik dźwiękowy WavPack áudio WavPack Áudio WavPack Audio WavPack аудио WavPack Zvuk WavPack Zvočna datoteka WavPack Audio WavPack Вејвпак звук WavPack-ljud WavPack sesi звук WavPack Âm thanh WavPack WavPack 音频 WavPack 音訊 WavPack audio correction file ملف تصحيح WavPack السمعي Fajł aŭdyjokarekcyi WavPack Файл за корекции на аудио — WavPack fitxer de correcció d'àudio de WavPack opravný zvukový soubor WavPack WavPack-lydkorrektionsfil WavPack-Audiokorrekturdatei Αρχείο διόρθωσης ήχου WavePack WavPack audio correction file archivo de corrección de sonido WavPack WavPack audio-zuzenketaren fitxategia WavPack-äänikorjaustiedosto WavPack ljóðrættingarfíla fichier de correction audio WavPack comhad cheartú fhuaim WavPack ficheiro de corrección de son WavPack קובץ תיקון שמע של WavPack WavPack datoteka ispravke zvuka WavPack hangjavítási fájl File de correction audio WavPack Berkas koreksi audio WavPack File correzione audio WavPack WavPack オーディオコレクションファイル WavPack аудио түзету файлы WavPack 오디오 교정 파일 WavPack garso korekcijos failas WavPack audio korekciju datne WavPack lydkorrigeringsfil WavPack-audio-correctiebestand WawPack lydopprettingsfil fichièr de correccion àudio WavPack Plik korekcji dźwięku WavPack ficheiro de correção áudio WavPack Arquivo de correção de áudio WavPack Fișier audio de corecție WavPack файл коррекции аудио WavPack Opravný zvukový súbor WavPack popravljalna zvočna datoteka WavPack File korrigjgimi audio WavPack датотека поправке Вејвпак звука WavPack-ljudkorrigeringsfil WavPack ses düzeltme dosyası файл корекції звуку WavPack Tập tin sửa chữa âm thanh WavPack WavPack 音频校正文档 WavPack 音訊校正檔 MIDI audio MIDI سمعي MIDI audio faylı Aŭdyjo MIDI Аудио — MIDI àudio MIDI zvuk MIDI Sain MIDI MIDI-lyd MIDI-Audio Ήχος MIDI MIDI audio MIDI-sondosiero sonido MIDI MIDI audioa MIDI-ääni MIDI ljóður audio MIDI fuaim MIDI son MIDI שמע MIDI MIDI audio MIDI hang Audio MIDI Audio MIDI Audio MIDI MIDI オーディオ MIDI аудиосы 미디 오디오 MIDI garso įrašas MIDI audio Audio MIDI MIDI-lyd MIDI-audio MIDI-lyd àudio MIDI Plik dźwiękowy MIDI áudio MIDI Áudio MIDI Audio MIDI аудио MIDI Zvuk MIDI Zvočna datoteka MIDI Audio MIDI МИДИ звук MIDI-ljud MIDI sesi звук MIDI Âm thanh MIDI MIDI 音频 MIDI 音訊 compressed Tracker audio Tracker سمعي مضغوط aŭdyjo skampresavanaha Trackera Аудио — Tracker, компресирано àudio Tracker amb compressió komprimovaný zvuk Tracker Trackerkomprimeret lyd Komprimiertes Tracker-Audio Συμπιεσμένος ήχος Tracker compressed Tracker audio sonido de Tracker comprimido konprimitutako Tracker audioa pakattu Tracker-ääni stappað Tracker ljóður audio Tracker compressé fuaim chomhbhrúite Tracker son comprimido de Tracker שמע גשש מכווץ komprimirani Tracker audio tömörített Tracker hang Audio Tracker comprimite audio Tracker terkompresi Audio compresso Tracker 圧縮 Tracker オーディオ сығылған Tracker аудиосы 압축된 Tracker 오디오 suglaudintas Tracker garso įrašas saspiests Tracker audio ingepakte Tracker-audio komprimert Tracker-lyd àudio Tracker compressat Skompresowany plik dźwiękowy Tracker áudio comprimido Tracker Áudio Tracker compactado Tracker audio comprimat аудио Tracker (сжатое) Komprimovaný zvuk Tracker Skrčena zvočna datoteka Tracker Audio Tracker e kompresuar запаковани звук Пратиоца komprimerat Tracker-ljud sıkıştırılmış Tracker sesi стиснутий звук Tracker âm thanh Tracker đã nén 压缩的 Tracker 音频 壓縮版 Tracker 音訊 AAC audio àudio AAC zvuk AAC AAC-lyd AAC-Audio Ήχος AAC AAC audio sonido AAC AAC audioa AAC-ääni audio AAC Son AAC שמע AAC AAC zvučni zapis AAC hang Audio ACC Audio AAC Audio AAC AAC オーディオ AAC аудиосы AAC 오디오 AAC audio àudio AAC Dźwięk AAC áudio AAC Áudio AAC аудио AAC Zvuk AAC Zvok AAC ААЦ звук AAC-ljud AAC sesi звукові дані AAC AAC 音频 AAC 音訊 AAC Advanced Audio Coding MPEG-4 audio MPEG-4 سمعي Aŭdyjo MPEG-4 Аудио — MPEG-4 àudio MPEG-4 zvuk MPEG-4 MPEG4-lyd MPEG-4-Audio Ήχος MPEG-4 MPEG-4 audio MPEG4-sondosiero sonido MPEG-4 MPEG-4 audioa MPEG-4-ääni MPEG-4 ljóður audio MPEG-4 fuaim MPEG-4 son MPEG-4 שמע MPEG-4 MPEG-4 audio MPEG-4 hang Audio MPEG-4 Audio MPEG-4 Audio MPEG-4 MPEG-4 オーディオ MPEG-4 აუდიო MPEG-4 аудиосы MPEG-4 오디오 MPEG-4 garso įrašas MPEG-4 audio MPEG-4-lyd MPEG4-audio MPEG-4-lyd àudio MPEG-4 Plik dźwiękowy MPEG-4 áudio MPEG-4 Áudio MPEG-4 Audio MPEG-4 аудио MPEG-4 Zvuk MPEG-4 Zvočna datoteka MPEG-4 Audio MPEG-4 МПЕГ-4 звук MPEG-4-ljud MPEG-4 sesi звук MPEG-4 Âm thanh MPEG-4 MPEG-4 音频 MPEG-4 音訊 MPEG-4 video MPEG-4 مرئي Videa MPEG-4 Видео — MPEG-4 vídeo MPEG-4 video MPEG-4 MPEG4-video MPEG-4-Video Βίντεο MPEG-4 MPEG-4 video MPEG-4-video vídeo MPEG-4 MPEG-4 bideoa MPEG-4-video MPEG-4 video vidéo MPEG-4 físeán MPEG-4 vídeo MPEG-4 וידאו MPEG-4 MPEG-4 video MPEG-4 videó Video MPEG-4 Video MPEG-4 Video MPEG-4 MPEG-4 動画 MPEG-4 ვიდეო MPEG-4 видеосы MPEG-4 동영상 MPEG-4 vaizdo įrašas MPEG-4 video MPEG-4-film MPEG4-video MPEG-4-video vidèo MPEG-4 Plik wideo MPEG-4 vídeo MPEG-4 Vídeo MPEG-4 Video MPEG-4 видео MPEG-4 Video MPEG-4 Video datoteka MPEG-4 Video MPEG-4 МПЕГ-4 видео MPEG-4-video MPEG-4 video відеокліп MPEG-4 Ảnh động MPEG-4 MPEG-4 视频 MPEG-4 視訊 MPEG-4 audio book كتاب MPEG-4 السمعي Aŭdyjokniha MPEG-4 Аудио книга — MPEG-4 llibre d'àudio MPEG-4 zvuková kniha MPEG-4 MPEG4-lydbog MPEG-4-Hörbuch Ηχητικό βιβλίο MPEG-4 MPEG-4 audio book MPEG-4-sonlibro audiolibro MPEG-4 MPEG-4 audio-liburua MPEG-4-äänikirja MPEG-4 ljóðbók livre audio MPEG-4 leabhar fhuaim MPEG-4 sonlibro de MPEG-4 ספר דיגיטלי MPEG-4 MPEG-4 audio knjiga MPEG-4 hangoskönyv Libro audio MPEG-4 Buku audio MPEG-4 Audiolibro MPEG-4 MPEG-4 オーディオブック MPEG-4 აუდიოწიგნი MPEG-4 аудио кітабы MPEG-4 오디오북 MPEG-4 garso knyga MPEG-4 audio grāmata MPEG-4-lydbok MPEG4-audioboek MPEG-4-lydbok libre àudio MPEG-4 Książka dźwiękowa MPEG-4 livro áudio MPEG-4 Áudio livro MPEG-4 Carte audio MPEG-4 аудиокнига MPEG-4 Zvuková kniha MPEG-4 Zvočna knjiga MPEG-4 Audiolibër MPEG-4 МПЕГ-4 звукотека MPEG-4-ljudbok MPEG-4 sesli kitabı аудіокнига MPEG-4 Sách âm thanh MPEG-4 MPEG-4 有声书 MPEG-4 音訊書 3GPP multimedia file ملف وسائط متعددة 3GPP Multymedyjny fajł 3GPP Мултимедия — 3GPP fitxer multimèdia 3GPP multimediální soubor 3GPP 3GPP multimedie-fil 3GPP-Multimediadatei Αρχείο πολυμέσων 3GPP 3GPP multimedia file archivo multimedia 3GPP 3GPP multimediako fitxategia 3GPP-multimediatiedosto 3GGP margmiðlafíla fichier multimédia 3GPP comhad ilmheán 3GPP ficheiro multimedia 3GPP קובץ מולטימדיה מסוג 3GPP 3GPP multimedijska datoteka 3GPP multimédiafájl File multimedial 3GPP Berkas multimedia 3GPP File multimediale 3GPP 3GPP マルチメディアファイル 3GPP მულტიმედიური ფაილი 3GPP мультимедиялық файлы 3GPP 멀티미디어 파일 3GPP multimedijos failas 3GPP multimediju datne 3GPP-multimediafil 3GPP-multimediabestand 3GPP-multimediafil fichièr multimèdia 3GPP Plik multimedialny 3GPP ficheiro multimédia 3GPP Arquivo multimídia 3GPP Fișier multimedia 3GPP мультимедийный файл 3GPP Súbor multimédií 3GPP Večpredstavnostna datoteka 3GPP File multimedial 3GPP 3ГПП мултимедијална датотека 3GPP-multimediafil 3GPP multimedya dosyası файл мультимедійних даних 3GPP Tập tin đa phương tiện 3GPP 3GPP 多媒体文件 3GPP 多媒體檔案 3GPP 3rd Generation Partnership Project 3GPP2 multimedia file ملف وسائط متعددة 3GPP2 Мултимедия — 3GPP2 fitxer multimèdia 3GPP2 multimediální soubor 3GPP2 3GPP2 multimedie-fil 3GPP2-Multimediadatei Αρχείο πολυμέσων 3GPP2 3GPP2 multimedia file archivo multimedia 3GPP2 3GPP2 multimediako fitxategia 3GPP2-multimediatiedosto 3GGP2 margmiðlafíla fichier multimédia 3GPP2 comhad ilmheán 3GPP2 ficheiro multimedia 3GPP2 קובץ מולטימדיה 3GPP2 3GPP2 multimedijska datoteka 3GPP2 multimédiafájl File multimedial 3GPP2 Berkas multimedia 3GPP2 File multimediale 3GPP2 3GPP2 マルチメディアファイル 3GPP2 მულტიმედიური ფაილი 3GPP2 мультимедиялық файлы 3GPP2 멀티미디어 파일 3GPP2 multimediju datne 3GPP2 multimedia bestand fichièr multimèdia 3GPP2 Plik multimedialny 3GPP2 ficheiro multimédia 3GPP2 Arquivo multimídia 3GPP2 Fișier multimedia 3GPP2 мультимедийный файл 3GPP2 Súbor multimédií 3GPP2 Večpredstavnostna datoteka 3GPP2 3ГПП2 мултимедијална датотека 3GPP2-multimediafil 3GPP2 multimedya dosyası файл мультимедійних даних 3GPP2 3GPP2 多媒体文件 3GPP2 多媒體檔案 3GPP2 3rd Generation Partnership Project 2 Amiga SoundTracker audio مقتفي صوت Amiga السمعي Aŭdyjo Amiga SoundTracker Аудио — Amiga SoundTracker àudio SoundTracker d'Amiga zvuk Amiga SoundTracker Amiga SoundTracker-lyd Amiga-SoundTracker-Audio Ήχος Amiga SoundTracker Amiga SoundTracker audio Sondosiero de Amiga SoundTracker sonido de Amiga SoundTracker Amiga soundtracker audioa Amiga SoundTracker -ääni Amiga SoundTracker ljóður audio SoundTracker Amiga fuaim Amiga SoundTracker son de Amiga SoundTracker קובץ שמע של Amiga SoundTracker Amiga SoundTracker audio Amiga SoundTracker hang Audio Amiga SoundTracker Audio Amida SoundTracker Audio Amiga SoundTracker Amiga SoundTracker オーディオ Amiga SoundTracker-ის აუდიო Amiga SoundTracker аудиосы Amiga SoundTracker 오디오 Amiga SoundTracker garso įrašas Amiga SoundTracker audio Audio Amiga Soundtracker Amiga SoundTracker-lyd Amiga SoundTracker-audio Amiga soundtracker-lyd àudio SoundTracker Amiga Plik dźwiękowy Amiga SoundTracker áudio SoundTracker do Amiga Áudio Amiga SoundTracker Audio Amiga SoundTracker аудио Amiga SoundTracker Zvuk Amiga SoundTracker Zvočna datoteka Amiga SoundTracker Audio Amiga SoundTracker звук Амигиног Пратиоца Звука Amiga SoundTracker-ljud Amiga SoundTracker sesi звук Amiga SoundTracker Âm thanh Amiga SoundTracker Amiga SoundTracker 音频 Amiga SoundTracker 音訊 MP2 audio MP2 سمعي Aŭdyjo MP2 Аудио — MP2 àudio MP2 zvuk MP2 MP2-lyd MP2-Audio Ήχος MP2 MP2 audio MP2-sondosiero sonido MP2 MP2 audioa MP2-ääni MP2 ljóður audio MP2 fuaim MP2 son MP2 שמע MP2 MP2 audio MP2 hang Audio MP2 Audio MP2 Audio MP2 MP2 オーディオ MP2 аудиосы MP2 오디오 MP2 garso įrašas MP2 audio MP2-lyd MP2-audio MP2-lyd àudio MP2 Plik dźwiękowy MP2 áudio MP2 Áudio MP2 Audio MP2 аудио MP2 Zvuk MP2 Zvočna datoteka MP2 Audio MP2 МП2 звук MP2-ljud MP2 sesi звук MP2 Âm thanh MP2 MP2 音频 MP2 音訊 MP3 audio MP3 سمعي MP3 audio faylı Aŭdyjo MP3 Аудио — MP3 àudio MP3 zvuk MP3 Sain MP3 MP3-lyd MP3-Audio Ήχος MP3 MP3 audio MP3-sondosiero sonido MP3 MP3 audioa MP3-ääni MP3 ljóður audio MP3 fuaim MP3 son MP3 שמע MP3 MP3 audio MP3 hang Audio MP3 Audio MP3 Audio MP3 MP3 オーディオ MP3 აუდიო MP3 аудиосы MP3 오디오 MP3 garso įrašas MP3 audio Audio MP3 MP3-lyd MP3-audio MP3-lyd àudio MP3 Plik dźwiękowy MP3 áudio MP3 Áudio MP3 Audio MP3 аудио MP3 Zvuk MP3 Zvočna datoteka MP3 Audio MP3 МП3 звук MP3-ljud MP3 sesi звук MP3 Âm thanh MP3 MP3 音频 MP3 音訊 MP3 audio (streamed) MP3 سمعي (تدفق) Aŭdyjo MP3 (płyń) Аудио — MP3, поточно àudio MP3 (flux) zvuk MP3 (proud) MP3-lyd (strøm) MP3-Audio (Stream) Ήχος MP3 (εκπεμπόμενος) MP3 audio (streamed) MP3-sondosiero (fluigate) sonido MP3 (en transmisión) MP3 audioa (korrontea) MP3-ääni (virtaus) MP3 ljóður (streymað) audio MP3 (flux) fuaim MP3 (sruthaithe) son MP3 (en stream) שמע MP3 (מוזרם) MP3 zvučni zapis (strujanje) MP3 hang (sugárzott) Audio MP3 (fluxo) Audio MP3 (stream) Audio MP3 (in streaming) MP3 オーディオ (ストリーム) MP3 აუდიო (ნაკადი) MP3 аудиосы (ағымдық) MP3 오디오(스트림) MP3 garso įrašas (transliuojamas) MP3 audio (straumēts) Audio MP3 (aliran) MP3-lyd (streaming) MP3-audio (gestreamd) Strauma MP3-lyd àudio MP3 (flux) Dźwięk MP3 (strumień) áudio MP3 (em fluxo) Áudio MP3 (em fluxo) Audio MP3 (flux) аудио MP3 (потоковое) Zvuk MP3 (streamovaný) Zvočna datoteka MP3 (pretočna) Audio MP3 (streamed) МП3 звук (проточан) MP3-ljud (flöde) MP3 sesi (akış) звук MP3 (потоковий) Âm thanh MP3 (chạy luồng) MP3 流音频 MP3 音訊 (串流) HTTP Live Streaming playlist قائمة بث HTTP حية Списък за изпълнение — поток по HTTP llista de reproducció en temps real HTTP seznam k přehrání HTTP Live Streaming Afspilningsliste til HTTP-livestrøm HTTP Live-Streaming-Wiedergabeliste Λίστα αναπαραγωγής ζωντανής μετάδοσης σε HTTP HTTP Live Streaming playlist lista de reproducción de flujo en directo HTTP HTTP zuzeneko korrontearen erreprodukzio-zerrenda HTTP beinleiðis streymaður avspælingarlisti liste de lecture de flux HTTP Live seinmliosta sruthaithe bheo HTTP lista de reprodución de fluxo HTTP רשימת השמעה הזרימה של HTTP HTTP popis izvođenja emitiranja uživo HTTP élő lejátszólista Lista de selection HTTP Live Streaming Daftar putar HTTP Live Streaming Playlist Live Steaming HTTP HTTP ライブストリーミング再生リスト HTTP тірі ағым ойнау тізімі HTTP 라이브 스트리밍 재생 목록 HTTP tiesioginio transliavimo grojaraštis HTTP dzīvās straumēšanas repertuārs HTTP Live Streaming afspeellijst lista de lectura de flux HTTP Live Lista odtwarzania strumieniowego na żywo HTTP lista de reprodução HTTP Live Streaming Lista de Reprodução Streaming ao Vivo de HTTP Listă de redare difuzată ca flux HTTP список воспроизведения HTTP-потока Zoznam stôp HTTP Live Streaming Seznam predvajanja živega pretoka HTTP ХТТП списак нумера Живог Протока HTTP Live Streaming-spellista HTTP Canlı Akış çalma listesi список відтворення HTTP Live Streaming HTTP 直播流播放列表 HTTP 即時串流播放清單 Microsoft ASX playlist قائمة تشغيل مايكروسوفت ASX Śpis Microsoft ASX Списък за изпълнение — Microsoft ASX llista de reproducció de Microsoft ASX seznam k přehrání Microsoft ASX Microsoft ASX-afspilningsliste Microsoft-ASX-Wiedergabeliste Λίστα αναπαραγωγής Microsoft ASX Microsoft ASX playlist lista de reproducción ASX de Microsoft Microsoft ASX erreprodukzio-zerrenda Microsoft ASX -soittolista Microsoft ASX avspælingarlisti liste de lecture Microsoft ASX seinmliosta Microsoft ASX lista de reprodución Microsoft ASX רשימת השמעה ASX (מיקרוסופט) Microsoft ASX popis za reprodukciju Microsoft ASX lejátszólista Lista de selection Microsoft ASX Senarai putar Microsoft ASX Playlist Microsoft ASX Microsoft ASX 再生リスト Microsoft-ის ASX რეპერტუარი Microsoft ASX ойнау тізімі Microsoft ASX 재생 목록 Microsoft ASX grojaraštis Microsoft ASX repertuārs Microsoft ASX-spilleliste Microsoft ASX-afspeellijst Microsoft ASX-speleliste lista de lectura Microsoft ASX Lista odtwarzania Microsoft ASX lista de reprodução Microsoft ASX Lista de reprodução do Microsoft ASX Listă redare Microsoft ASX список воспроизведения Microsoft ASX Zoznam skladieb Microsoft ASX Seznam predvajanja Microsoft ASX Listë titujsh Microsoft ASF Мајкрософтов АСИкс списак нумера Microsoft ASX-spellista Microsoft ASX çalma listesi список відтворення ASX Microsoft Danh mục nhạc Microsoft ASX Microsoft ASX 播放列表 微軟 ASX 播放清單 PSF audio PSF سمعي Aŭdyjo PSF Аудио — PSF àudio PSF zvuk PSF PSF-lyd PSF-Audio Ήχος PSF PSF audio PSF-sondosiero sonido PSF PSF audioa PSF-ääni PSF ljóður audio PSF fuaim PSF son PSF שמע PSF PSF zvučni zapis PSF hang Audio PSF Audio PSF Audio PSF PSF オーディオ PSF аудиосы PSF 오디오 PSF garso įrašas PSF audio PSF-lyd PSF-audio PSF-lyd àudio PSF Plik dźwiękowy PSF áudio PSF Áudio PSF Audio PSF аудио PSF Zvuk PSF Zvočna datoteka PSF Audio PSF ПСФ звук PSF-ljud PSF sesi звук PSF Âm thanh PSF PSF 音频 PSF 音訊 PSF Portable Sound Format MiniPSF audio MiniPSF سمعي Aŭdyjo MiniPSF Аудио — MiniPSF àudio MiniPSF zvuk MiniPSF MiniPSF-lyd MiniPSF-Audio Ήχος MiniPSF MiniPSF audio MiniPSF-sondosiero sonido MiniPSF MiniPSF audioa MiniPSF-ääni MiniPSF ljóður audio MiniPSF fuaim MiniPSF son MiniPSF שמע של MiniPSP MiniPSF audio MiniPSF hang Audio MiniPSF Audio MiniPSF Audio MiniPSF MiniPSF オーディオ MiniPSF-ის აუდიო MiniPSF аудиосы MiniPSF 오디오 MiniPSF garso įrašas MiniPSF audio MiniPSF-lyd MiniPSF-audio MiniPSF-lyd àudio MiniPSF Plik dźwiękowy MiniPSF áudio MiniPSF Áudio MiniPSF Audio MiniPSF аудио MiniPSF Zvuk MiniPSF Zvočna datoteka MiniPSF Audio MiniPSF Мини ПСФ звук MiniPSF-ljud MiniPSF sesi звук MiniPSF Âm thanh MiniPSF MiniPSF 音频 MiniPSF 音訊 MiniPSF Miniature Portable Sound Format PSFlib audio library مكتبة PSFlib السمعية Aŭdyjobiblijateka PSFlib Аудио библиотека — PSFlib biblioteca d'àudio PSFlib zvuková knihovna PSFlib PSFlib-lydbibliotek PSFlib-Audiobibliothek Βιβλιοθήκη ήχου PSFlib PSFlib audio library biblioteca de sonido PSFlib PSFlib audioaren liburutegia PSFlib-äänikirjasto PSFlib ljóðsavn bibliothèque audio PSFlib leabharlann fhuaim PSFlib Biblioteca de son PSFlib ספריית שמע PSFlib PSFlib zvučna biblioteka PSFlib hanggyűjtemény Bibliotheca audio PSFlib Pustaka audio PSFlib Libreria audio PSFlib PSFlib オーディオライブラリ PSFlib аудио жинағы PSFlib 오디오 라이브러리 PSFlib garso biblioteka PSFlib fonotēka PSFlib-lydbibliotek PSFlib-audiobibliotheek PSFlib lydbibliotek bibliotèca àudio PSFlib Biblioteka dźwiękowa PSFlib biblioteca áudio PSFlib Biblioteca de áudio PSFlib Bibliotecă audio PSFlib фонотека PSFlib Zvuková knižnica PSFlib Zvočna knjižnica PSFlib Librari audio PSFlib библиотека звука ПСФ библиотеке PSFlib-ljudbibliotek PSFlib ses kitaplığı аудіобібліотека PSFlib Thư viện âm thanh PSFlib PSFlib 音频库文件 PSFlib 音訊庫 PSFlib Portable Sound Format Library Windows Media audio Windows Media سمعي Aŭdyjo Windows Media Аудио — Windows Media àudio de Windows Media zvuk Windows Media Windows Media-lyd Windows-Media-Audio Ήχος Windows Media Windows Media audio sonido de Windows Media Windows Media audioa Windows Media -ääni Windows Media ljóður audio Windows Media fuaim Windows Media son de Windows Media שמע של Windows Media Windows Media audio Windows Media hang Audio Windows Media Audio Windows Media Audio Windows Media Windows Media オーディオ Windows Media аудиосы Windows 미디어 오디오 Windows Media garso įrašas Windows Media audio Windows Media lyd Windows Media-audio Windows Media-lyd àudio Windows Media Plik dźwiękowy Windows Media áudio Windows Media Áudio do Windows Media Audio Windows Media аудио Windows Media Zvuk Windows Media Zvočna datoteka Windows Media Audio Windows Media Виндоуз Медија звук Windows Media-ljud Windows Media sesi звук Windows Media Âm thanh Windows Media Windows Media 音频 Windows Media 音訊 Musepack audio Musepack سمعي Aŭdyjo Musepack Аудио — Musepack àudio de Musepack zvuk Musepack Musepacklyd Musepack-Audio Ήχος Musepack Musepack audio sonido Musepack Musepack audioa Musepack-ääni Musepack ljóður audio Musepack fuaim Musepack son de Musepack שמע של Musepack Musepack audio Musepack hang Audio Musepack Audio Musepack Audio Musepack Musepack オーディオ Musepack аудиосы Musepack 오디오 Musepack garso įrašas Musepack audio Musepack-lyd Musepack-audio Musepack-lyd àudio Musepack Plik dźwiękowy Musepack áudio Musepack Áudio Musepack Audio Musepack аудио Musepack Zvuk Musepack Zvočna datoteka Musepack Audio Musepack звук Мјузпака Musepack-ljud Musepack sesi звук Musepack Âm thanh Musepack Musepack 音频 Musepack 音訊 RealAudio document مستند RealAudio Dakument RealAudio Документ — RealAudio document RealAudio dokument RealAudio RealAudio-dokument RealAudio-Dokument Έγγραφο RealAudio RealAudio document RealAudio-dokumento documento RealAudio RealAudio dokumentua RealAudio-asiakirja RealAudio skjal document RealAudio cáipéis RealAudio documento Realson מסמך של RealAudio RealAudio dokument RealAudio dokumentum Documento RealAudio Dokumen RealAudio Documento RealAudio RealAudio ドキュメント RealAudio құжаты RealAudio 문서 RealAudio dokumentas RealAudio dokuments RealAudio-dokument RealAudio-document RealAudio-dokument document RealAudio Dokument RealAudio documento RealAudio Documento RealAudio Document RealAudio документ RealAudio Dokument RealAudio Dokument RealAudio Dokument RealAudio документ Рил Аудиа RealAudio-dokument RealAudio belgesi документ RealAudio Tài liệu âm thanh RealAudio RealAudio 文档 RealAudio 文件 RealMedia Metafile ملف تعريف RealMedia Metafajł RealMedia Метафайл — RealMedia metafitxer RealMedia RealMedia Metafile RealMedia-metafil RealMedia-Metadatei Metafile RealMedia RealMedia Metafile metaarchivo RealMedia RealMedia metafitxategia RealMedia-metatiedosto RealMedia metafíla métafichier RealMedia meiteachomhad RealMedia Metaficheiro RealMedia קובץ מטא של RealMedia RealMedia meta datoteka RealMedia metafájl Metafile RealMedia RealMedia Metafile Metafile RealMedia RealMedia メタファイル RealMedia метафайлы RealMedia 메타 파일 RealMedia metafailas RealMedia metadatne RealMedia-metafil RealMedia-metabestand RealMedia-metafil metafichièr RealMedia Metaplik RealMedia metaficheiro RealMedia Meta arquivo do RealMedia Metafișier RealMedia мета-файл RealMedia RealMedia Metafile Metadatoteka RealMedia Metafile RealMedia метадатотека Рил Медија RealMedia-metafil RealMedia Meta Dosyası метафайл RealMedia Siêu tập tin RealMedia RealMedia 元文件 RealMedia 中介檔 RealVideo document مستند RealVideo Dakument RealVideo Документ — RealVideo document RealVideo dokument RealVideo RealAudio-dokument RealVideo-Dokument Έγγραφο RealVideo RealVideo document RealVideo-dokumento documento RealVideo RealVideo dokumentua RealVideo-asiakirja RealVideo skjal document RealVideo cáipéis RealVideo documento RealVideo מסמך של RealVideo RealVideo dokument RealVideo dokumentum Documento RealVideo Dokumen RealVideo Documento RealVideo RealVideo ドキュメント RealVideo құжаты RealVideo 문서 RealVideo dokumentas RealVideo dokuments RealAudio-dokument RealVideo-document RealVideo-dokument document RealVideo Dokument RealVideo documento RealVideo Documento RealVideo Document RealVideo документ RealVideo Dokument RealVideo Video datoteka RealVideo Dokument RealVideo документ Рил Видеа RealVideo-dokument RealAudio belgesi документ RealVideo Tài liệu ảnh động RealVideo RealAudio 文档 RealVideo 文件 RealMedia document مستند RealMedia Dakument RealMedia Документ — RealMedia document RealMedia dokument RealMedia RealMedia-dokument RealMedia-Dokument Έγγραφο RealMedia RealMedia document RealMedia-dokumento documento RealMedia RealMedia dokumentua RealMedia-asiakirja RealMedia skjal document RealMedia cáipéis RealMedia documento RealMedia מסמך של RealMedia RealMedia dokument RealMedia dokumentum Documento RealMedia Dokumen RealMedia Documento RealMedia RealMedia ドキュメント RealMedia құжаты RealMedia 문서 RealMedia dokumentas RealMedia dokuments RealMedia-dokument RealMedia-document RealMedia-dokument document RealMedia Dokument RealMedia documento RealMedia Documento RealMedia Document RealMedia документ RealMedia Dokument RealMedia Dokument RealMedia Dokument RealMedia документ Рил Медија RealMedia-dokument RealMedia belgesi документ RealMedia Tài liệu RealMedia RealMedia 文档 RealMedia 文件 RealPix document مستند RealPix Dakument RealPix Документ — RealPix document RealPix dokument RealPix RealPix-dokument RealPix-Dokument Έγγραφο RealPix RealPix document RealPix-dokumento documento RealPix RealPix dokumentua RealPix-asiakirja RealPix skjal document RealPix cáipéis RealPix documento RealPix מסמך של RealPix RealPix dokument RealPix dokumentum Documento RealPix Dokumen RealPix Documento RealPix RealPix ドキュメント RealPix құжаты RealPix 문서 RealPix dokumentas RealPix dokuments RealPix-dokument RealPix-document RealPix-dokument document RealPix Dokument RealPix documento RealPix Documento RealPix Document RealPix документ RealPix Dokument RealPix Dokument RealPix Dokument RealPix документ Рил Пикса RealPix-dokument RealPix belgesi документ RealPix Tài liệu ảnh RealPix RealPix 文档 RealPix 文件 RealText document مستند RealText Dakument RealText Документ — RealText document RealText dokument RealText RealText-dokument RealText-Dokument Έγγραφο RealText RealText document RealText-dokumento documento RealText RealText dokumentua RealText-asiakirja RealText skjal document RealText cáipéis RealText documento RealText מסמך של RealText RealText dokument RealText dokumentum Documento RealText Dokumen RealText Documento RealText RealText ドキュメント RealText құжаты RealText 문서 RealText dokumentas RealText dokuments RealText-dokument RealText-document RealText-dokument document RealText Dokument RealText documento RealText Documento RealText Document RealText документ RealText Dokument RealText Dokument RealText Dokument RealText документ Рил Текста RealText-dokument RealText belgesi документ RealText Tài liệu văn bản RealText RealText 文档 RealText 文件 RIFF audio RIFF سمعي RIFF audio faylı Aŭdyjo RIFF Аудио — RIFF àudio RIFF zvuk RIFF Sain RIFF RIFF-lyd RIFF-Audio Ήχος RIFF RIFF audio RIFF-sondosiero sonido RIFF RIFF audioa RIFF-ääni RIFF ljóð audio RIFF fuaim RIFF son RIFF שמע RIFF RIFF audio RIFF-kép Audio RIFF Audio RIFF Audio RIFF RIFF オーディオ RIFF аудиосы RIFF 오디오 RIFF garso įrašas RIFF audio Audio RIFF RIFF-lyd RIFF-audio RIFF-lyd àudio RIFF Plik dźwiękowy RIFF áudio RIFF Áudio RIFF Audio RIFF аудио RIFF Zvuk RIFF Zvočna datoteka RIFF Audio RIFF РИФФ звук RIFF-ljud RIFF sesi звук RIFF Âm thanh RIFF RIFF 音频 RIFF 音訊 RIFF container contenidor RIFF kontejner RIFF RIFF-container RIFF-Container Περιέκτης RIFF RIFF container contenedor RIFF RIFF edukitzailea conteneur RIFF Contenedor RIFF מכולת RIFF RIFF spremnik RIFF konténer Receptaculo RIFF Wadah RIFF Container RIFF RIFF контейнері RIFF 컨테이너 contenidor RIFF Kontener RIFF contentor RIFF Contêiner RIFF Контейнер RIFF Kontajner RIFF Vsebnik RIFF РИФФ садржалац RIFF-behållare RIFF deposu контейнер RIFF RIFF 容器 RIFF 容器 Scream Tracker 3 audio Scream Tracker 3 سمعي Scream Tracker 3 audio faylı Aŭdyjo Scream Tracker 3 Аудио — Scream Tracker 3 àudio de Scream Tracker 3 skladba Scream Tracker 3 Sain Scream Tracker 3 Scream Tracker 3-lyd Scream-Tracker-3-Audio Ήχος Scream Tracker 3 Scream Tracker 3 audio Sondosiero de Scream Tracker 3 sonido Scream Tracker 3 Scream Tracker 3 audioa Scream Tracker 3 -ääni Scream Tracker 3 ljóður audio Scream Tracker 3 fuaim Scream Tracker 3 son Scream Tracker 3 שמע של Scream Tracker 3 Scream Tracker 3 audio Scream Tracker 3 hang Audio Scream Tracker 3 Audio Scream Tracker 3 Audio Scream Tracker 3 Scream Tracker 3 オーディオ Scream Tracker 3 аудиосы Scream Tracker 3 오디오 Scream Tracker 3 garso įrašas Scream Tracker 3 audio Audio Scream Tracker 3 Scream Tracker 3-lyd Scream Tracker 3-audio Sream Tracker 3 lyd àudio Scream Tracker 3 Plik dźwiękowy Scream Tracker 3 áudio Scream Tracker 3 Áudio Scream Tracker 3 Audio Scream Tracker 3 аудио Scream Tracker 3 Skladba Scream Tracker 3 Zvočna datoteka Scream Tracker 3 Audio Scream Tracker 3 звук Скрим Тракера 3 Scream Tracker 3-ljud Scream Tracker 3 sesi звук Scream Tracker 3 Âm thanh Scream Tracker 3 Scheme Tracker 3 音频 Scream Tracker 3 音訊 MP3 ShoutCast playlist قائمة تشغيل MP3 ShoutCast Śpis piesień dla tranślacyi MP3 Списък за изпълнение — MP3 ShoutCast llista de reproducció MP3 ShoutCast seznam k přehrání MP3 ShoutCast MP3 ShoutCast-afspilningsliste MP3-ShoutCast-Wiedergabeliste Λίστα αναπαραγωγής MP3 ShoutCast MP3 ShoutCast playlist MP3-ludlisto de ShoutCast lista de reproducción MP3 ShoutCast MP3 ShoutCast erreprodukzio-zerrenda MP3 ShoutCast -soittolista MP3 ShoutCast avspælingarlisti liste de lecture MP3 ShoutCast seinmliosta MP3 ShoutCast lista de reprodución MP3 de ShoutCast רשימת השמעה MP3 של ShoutCast MP3 ShoutCast popis za reprodukciju MP3 ShoutCast-lejátszólista Lista de selection MP3 ShoutCast Senarai putar MP3 ShoutCast Playlist MP3 ShoutCast MP3 ShoutCast 再生リスト MP3 ShoutCast ойнау тізімі MP3 ShoutCast 재생 목록 MP3 ShoutCast grojaraštis MP3 ShoutCast repertuārs Senaraimain ShoutCast MP3 MP3 ShoutCast-spilleliste MP3 ShoutCast-afspeellijst MP3 ShoutCast-speleliste lista de lectura MP3 ShoutCast Lista odtwarzania MP3 ShoutCast lista de reprodução MP3 ShoutCast Lista de reprodução MP3 ShoutCast Listă MP3 ShoutCast список воспроизведения MP3 ShoutCast Zoznam skladieb MP3 ShoutCast Seznam predvajanja MP3 ShoutCast Listë titujsh MP3 ShoutCast списак МП3 песама Шаут Каста MP3 ShoutCast-spellista MP3 ShoutCast çalma listesi список програвання MP3 ShoutCast Danh mục nhạc MP3 ShoutCast MP3 ShoutCast 播放列表 MP3 ShoutCast 播放清單 Scream Tracker audio Scream Tracker سمعي Scream Tracker audio faylı Aŭdyjo Scream Tracker Аудио — Scream Tracker àudio de Scream Tracker skladba Scream Tracker Sain Scream Tracker Scream Tracker-lyd Scream-Tracker-Audio Ήχος Scream Tracker Scream Tracker audio Sondosiero de Scream Tracker sonido Scream Tracker Scream Tracker audioa Scream Tracker -ääni Scream Tracker ljóður audio Scream Tracker fuaim Scream Tracker son Scream Tracker שמע של Scream Tracker Scream Tracker audio Scream Tracker hang Audio Scream Tracker Audio Scream Tracker Audio Scream Tracker Scream Tracker オーディオ Scream Tracker аудиосы Scream Tracker 오디오 Scream Tracker garso įrašas Scream Tracker audio Audio Scream Tracker Scream Tracker-lyd Scream Tracker-audio Scream Tracker lyd àudio Scream Tracker Plik dźwiękowy Scream Tracker áudio Scream Tracker Áudio Scream Tracker Audio Scream Tracker аудио Scream Tracker Skladba Scream Tracker Zvočna datoteka Scream Tracker Audio Scream Tracker звук Скрим Тракера Scream Tracker-ljud Scream Tracker sesi звук Scream Tracker Âm thanh Scream Tracker Scream Tracker 音频 Scream Tracker 音訊 VOC audio VOC سمعي VOC audio faylı Aŭdyjo VOC Аудио — VOC àudio VOC zvuk VOC Sain VOC VOC-lyd VOC-Audio Ήχος VOC VOC audio VOC-sondosiero sonido VOC VOC audioa VOC-ääni VOC ljóður audio VOC fuaim VOC son VOC שמע VOC VOC audio VOC hang Audio VOC Audio VOC Audio VOC VOC オーディオ VOC аудиосы VOC 오디오 VOC garso įrašas VOC audio Audio VOC VOC-lyd VOC-audio VOC-lyd àudio VOC Plik dźwiękowy VOC áudio VOC Áudio VOC Audio VOC аудио VOC Zvuk VOC Zvočna datoteka VOC Audio VOC ВОЦ звук VOC-ljud VOC sesi звук VOC Âm thanh VOC VOC 音频 VOC 音訊 WAV audio WAV سمعي WAV audio faylı Aŭdyjo WAV Аудио — WAV àudio WAV zvuk WAV Sain WAV WAV-lyd WAV-Audio Ήχος WAV WAV audio WAV-sonkodo sonido WAV WAV audioa WAV-ääni WAV ljóður audio WAV fuaim WAV son WAV שמע WAV WAV audio WAV hang Audio WAV Audio WAV Audio WAV WAV オーディオ WAV аудиосы WAV 오디오 WAV garso įrašas WAV audio Audio VOC WAV-lyd WAV-audio WAV-lyd àudio WAV Plik dźwiękowy WAV áudio WAV Áudio WAV Audio WAV аудио WAV Zvuk WAV Zvočna datoteka WAV Audio WAV ВАВ звук WAV-ljud WAV sesi звук WAV Âm thanh WAV WAV 音频 WAV 音訊 Scream Tracker instrument آلة Scream Tracker Scream Tracker instrumenti Instrument Scream Tracker Инструмент — Scream Tracker instrument de Scream Tracker nástroj pro Scream Tracker Offeryn Scream Tracker Scream Tracker-instrument Scream-Tracker-Instrument Μουσικό όργανο Scream Tracker Scream Tracker instrument instrumento de Scream Tracker instrumento Scream Tracker Scream Tracker instrumentua Scream Tracker -soitin Scream Tracker ljóðføri instrument Scream Tracker ionstraim Scream Tracker Instrumento Scream Tracker כלי של Scream Tracker Scream Tracker instrument Scream Tracker hangszer Instrumento Scream Tracker Instrumen Scream Tracker Strumento Scream Tracker Scream Tracker インストゥルメント Scream Tracker сайманы Scream Tracker 악기 Scream Tracker instrumentas Scream Tracker instrumenti Instrumen Scream Tracker Scream Tracker-instrument Scream Tracker-instrument Scream Tracker instrument instrument Scream Tracker Instrument Scream Tracker instrumento Scream Tracker Instrumento Scream Tracker Instrument Scream Tracker инструмент Scream Tracker Nástroj pre Scream Tracker Datoteka zvoka glasbila Scream Tracker Instrument Scream Tracker инструмент Скрим Тракера Scream Tracker-instrument Scream Tracker çalgısı інструмент Scream Tracker Nhạc khí Scream Tracker Scream Tracker 乐器 Scream Tracker 樂器檔 FastTracker II audio FastTracker II سمعي FastTracker II audio faylı Aŭdyjo FastTracker II Аудио — FastTracker II àudio de FastTracker II zvuk FastTracker II Sain FastTracker II FastTracker II-lyd FastTracker-II-Audio Ήχος FastTracker II FastTracker II audio Sondosiero de FastTracker II sonido FastTracker II FastTracker II.ren audioa FastTracker II -ääni FastTracker II ljóður audio FastTracker II fuaim FastTracker II son de FastTracker II שמע FastTracker II FastTracker II audio FastTracker II hang Audio FastTracker II Audio FastTracker II Audio FastTracker II FastTracker II オーディオ FastTracker II-ის აუდიო FastTracker II аудиосы FastTracker II 오디오 FastTracker II garso įrašas FastTracker II audio Audio FastTracker II FastTracker II-lyd FastTracker II-audio FastTracker II lyd àudio FastTracker II Plik dźwiękowy FastTracker II áudio FastTracker II Áudio FastTracker II Audio FastTracker II аудио FastTracker II Zvuk FastTracker II Zvočna datoteka FastTracker II Audio FastTracker II звук Фаст Тракера ИИ FastTracker II-ljud FastTracker II sesi звук FastTracker II Âm thanh FastTracker II FastTracker II 音频 FastTracker II 音訊 TrueAudio audio TrueAudio سمعي Aŭdyjo TrueAudio Аудио — TrueAudio àudio de TrueAudio zvuk TrueAudio TrueAudio-lyd TrueAudio-Audio Ήχος TrueAudio TrueAudio audio TrueAudio-sondosiero sonido TrueAudio TrueAudio audioa TrueAudio-ääni TrueAudio ljóður audio TrueAudio fuaim TrueAudio son Trueson שמע TrueAudio TrueAudio audio TrueAudio hang Audio TrueAudio Audio TrueAudio Audio TrueAudio TrueAudio オーディオ TrueAudio аудиосы TrueAudio 오디오 TrueAudio garso įrašas TrueAudio audio TrueAudio-lyd TrueAudio-audio TrueAudio-lyd àudio TrueAudio Plik dźwiękowy TrueAudio áudio TrueAudio Áudio TrueAudio Audio TrueAudio аудио TrueAudio Zvuk TrueAudio Zvočna datoteka TrueAudio Audio TrueAudio Тру Аудио звук TrueAudio-ljud TrueAudio sesi звук TrueAudio Âm thanh TrueAudio TrueAudio 音频 TrueAudio 音訊 Windows BMP image صورة Windows BMP Windows BMP rəsmi Vyjava Windows BMP Изображение — Windows BMP imatge BMP de Windows obrázek Windows BMP Delwedd BMP Windows Windows BMP-billede Windows-BMP-Bild Εικόνα Windows BMP Windows BMP image BMP-bildo de Vindozo imagen BMP de Windows Windows BMP irudia Windows BMP -kuva Windows BMP mynd image Windows BMP íomhá BMP Windows imaxe BMP de Windows תמונת BMP של Windows Windows BMP slika Windows BMP-kép Imagine BMP de Windows Citra Windows BMP Immagine Windows BMP Windows BMP 画像 Windows BMP суреті Windows BMP 그림 Windows BMP paveikslėlis Windows BMP attēls Imej BMP Windows Windows BMP-bilde Windows BMP-afbeelding Windows BMP-bilete imatge Windows BMP Obraz BMP Windows imagem BMP Windows Imagem BMP do Windows Imagine Windows BMP изображение Windows BMP Obrázok Windows BMP Slikovna datoteka Windows BMP Figurë Windows BMP Виндоузова БМП слика Windows BMP-bild Windows BMP görüntüsü зображення Windows BMP Ảnh BMP Windows Windows BMP 图像 Windows BMP 影像 WBMP image صورة WBMP Vyjava WBMP Изображение — WBMP imatge WBMP obrázek WBMP WBMP-billede WBMP-Bild Εικόνα WBMP WBMP image WBMP-bildo imagen WBMP WBMP irudia WBMP-kuva WBMP mynd image WBMP íomhá WBMP imaxe WBMP תמונת WBMP WBMP slika WBMP kép Imagine WBMP Citra WBMP Immagine WBMP WBMP 画像 WBMP суреті WBMP 그림 WBMP paveikslėlis WBMP attēls WBMP-bilde WBMP-afbeelding WBMP-bilete imatge WBMP Obraz WBMP imagem WBMP Imagem WBMP Imagine WBMP изображение WBMP Obrázok WBMP Slikovna datoteka WBMP Figurë WBMP ВБМП слика WBMP-bild WBMP görüntüsü зображення WBMP Ảnh WBMP WBMP 图像 WBMP 影像 WBMP WAP bitmap Computer Graphics Metafile ملف تعريف رسوميات الحاسوب Kompüter Qrafikası Meta Faylı Metafajł Computer Graphics Метафайл — Computer Graphics metafitxer de Computer Graphics Computer Graphics Metafile Delwedd ffurf CGM Computer Graphics-metafil CGM-Datei Αρχείο Computer Graphics Metafile Computer Graphics Metafile metaarchivo de Computer Graphics Ordenagailuko grafikoen meta-fitxategia Computer Graphics -metatiedosto Teldugrafikk metafíla métafichier Computer Graphics meiteachomhad Grafaicí Ríomhaire metaficheiro de Computer Graphics קובץ-מטה מסוג Computer Graphics Computer Graphics meta datoteka Computer Graphics-metafájl Metafile Computer Graphics Computer Graphics Metafile Computer Graphics Metafile コンピューターグラフィックメタファイル компьютерлік графика метафайлы 컴퓨터 그래픽스 메타 파일 Computer Graphics metafailas Datorgrafikas metadatne Failmeta Grafik Komputer Computer Graphics Metafile Computer Graphics-metabestand Computer Graphics Metafile metafichièr Computer Graphics Metaplik grafiki komputerowej (CGM) metaficheiro Computer Graphics Meta-arquivo do Computer Graphics Metafișier Computer Graphics метафайл компьютерной графики Computer Graphics Metafile Metadatoteka računalniške grafike (CGM) Metafile Computer Graphics Метадатотека рачунарске графике Computer Graphics Metafil Computer Graphics Meta dosyası метафайл комп'ютерної графіки Siêu tập tin đồ họa máy tính (CMF) CGM 计算机图像元文件 CGM 影像 CCITT G3 fax فاكس CCITT G3 Faks CCITT G3 Факс — CCITT G3 fax CCITT G3 fax CCITT G3 CCITT G3-fax CCITT-G3-Fax φαξ σε μορφή CCITT G3 CCITT G3 fax G3-fakso de CCITT fax de CCITT G3 CCITT G3 faxa CCITT G3 -faksi CCITT G3 telefaks télécopie G3 CCITT facs CCITT G3 fax de CCITT G3 פקס של CCITT G3 CCITT G3 faks CCITT G3-fax Fax CCITT G3 Faks CCITT G3 Fax CCITT G3 CCITT G3 FAX CCITT G3 ფაქსი CCITT G3 факсі CCITT G3 팩스 CCITT G3 faksas CCITT G3 fakss Faks g3 CCITT CCITT G3-faks CCITT G3-fax CCITT G3-fax telecòpia G3 CCITT Faks CCITT G3 fax CCITT G3 Fax do CCITT G3 Fax CCITT G3 факс CCITT G3 Fax CCITT G3 Datoteka faksimila CCITT G3 Fax CCITT G3 ЦЦИТТ Г3 факс CCITT G3-fax CCITT G3 faksı факс CCITT G3 Điện thư G3 CCITT CCITT G3 传真 CCITT G3 傳真檔 G3 fax image صورة فاكس G3 G3 faks rəsmi Faksavaja vyjava G3 Изображение — факс G3 imatge de fax G3 faxový obrázek G3 Delwedd Ffacs G3 G3-faxbillede G3-Faxbild Εικόνα φαξ G3 G3 fax image G3-faksbildo imagen de fax G3 G3 fax-irudia G3-faksikuva G3 fax mynd image de télécopie G3 íomhá fhacs G3 imaxe de fax G3 תמונת פקס של G3 G3 slika faksa G3-faxkép Imagine de fax G3 Citra faks G3 Immagine fax G3 G3 FAX 画像 G3 fax გამოსახულება G3 факс суреті G3 팩스 그림 G3 fax paveikslėlis G3 faksa attēls Imej fax G3 G3-faksbilde G3 faxafbeelding G3 faksbilete imatge de telecòpia G3 Obraz faksowy G3 imagem de fax G3 Imagem de fax G3 Imagine fax G3 факсовое изображение G3 Obrázok fax G3 Slikovna datoteka G3 fax Figurë Fax G3 слика Г3 факса G3-faxbild G3 fax görüntüsü факс G3 Ảnh điện thư G3 G3 传真文档 G3 傳真圖 GIF image صورة GIF GIF rəsmi Vyjava GIF Изображение — GIF imatge GIF obrázek GIF Delwedd GIF GIF-billede GIF-Bild Εικόνα GIF GIF image GIF-bildo imagen GIF GIF irudia GIF-kuva GIF mynd image GIF íomhá GIF imaxe GIF תמונת GIF GIF slika GIF-kép Imagine GIF Citra GIF Immagine GIF GIF 画像 GIF გამოსახულება GIF суреті GIF 그림 GIF paveikslėlis GIF attēls Imej GIF GIF-bilde GIF-afbeelding GIF-bilete imatge GIF Obraz GIF imagem GIF Imagem GIF Imagine GIF изображение GIF Obrázok GIF Slikovna datoteka GIF Figurë GIF ГИФ слика GIF-bild GIF görüntüsü зображення GIF Ảnh GIF GIF 图像 GIF 影像 IEF image صورة IEF IEF rəsmi Vyjava IEF Изображение — IEF imatge IEF obrázek IEF Delwedd IEF IEF-billede IEF-Bild Εικόνα IEF IEF image IEF-bildo imagen IEF IEF irudia IEF-kuva IEF mynd image IEF íomhá IEF imaxe IEF תמונת IEF IEF slika IEF-kép Imagine IEF Citra IEF Immagine IEF IEF 画像 IEF суреті IEF 그림 IEF paveikslėlis IEF attēls Imej IEF IEF-bilde IEF-afbeelding IEF-bilete imatge IEF Obraz IEF imagem IEF Imagem IEF Imagine IEF изображение IEF Obrázok IEF Slikovna datoteka IEF Figurë IEF ИЕФ слика IEF-bild IEF görüntüsü зображення IEF Ảnh IEF IEF 图像 IEF 影像 JPEG image صورة JPEG JPEG rəsmi Vyjava JPEG Изображение — JPEG imatge JPEG obrázek JPEG Delwedd JPEG JPEG-billede JPEG-Bild Εικόνα JPEG JPEG image JPEG-bildo imagen JPEG JPEG irudia JPEG-kuva JPEG mynd image JPEG íomhá JPEG imaxe JPEG תמונת JPEG JPEG slika JPEG-kép Imagine JPEG Citra JPEG Immagine JPEG JPEG 画像 JPEG суреті JPEG 그림 JPEG paveikslėlis JPEG attēls Imej JPEG JPEG-bilde JPEG-afbeelding JPEG-bilete imatge JPEG Obraz JPEG imagem JPEG Imagem JPEG Imagine JPEG изображение JPEG Obrázok JPEG Slikovna datoteka JPEG Figurë JPEG ЈПЕГ слика JPEG-bild JPEG görüntüsü зображення JPEG Ảnh JPEG JPEG 图像 JPEG 影像 JPEG-2000 image صورة JPEG-2000 Vyjava JPEG-2000 Изображение — JPEG-2000 imatge JPEG-2000 obrázek JPEG-2000 JPEG2000-billede JPEG-2000-Bild Εικόνα JPEG-2000 JPEG-2000 image JPEG-2000-bildo imagen JPEG-2000 JPEG-2000 irudia JPEG-2000-kuva JPEG-2000 mynd image JPEG-2000 íomhá JPEG-2000 imaxe JPEG-2000 תמונת JPEG-2000 JPEG-2000 slika JPEG-2000 kép Imagine JPEG-2000 Citra JPEG-2000 Immagine JPEG-2000 JPEG-2000 画像 JPEG-2000 суреті JPEG-2000 그림 JPEG-2000 paveikslėlis JPEG-2000 attēls Imej JPEG-2000 JPEG-2000-bilde JPEG-2000-afbeelding JPEG-2000-bilete imatge JPEG-2000 Obraz JPEG-2000 imagem JPEG-2000 Imagem JPEG-2000 Imagine JPEG-2000 изображение JPEG-2000 Obrázok JPEG-2000 Slikovna datoteka JPEG-2000 Figurë JPEG-2000 ЈПЕГ-2000 слика JPEG-2000-bild JPEG-2000 görüntüsü зображення JPEG-2000 Ảnh JPEG-2000 JPEG-2000 图像 JPEG-2000 影像 OpenRaster archiving image صورة أرشيف OpenRaster Изображение — OpenRaster imatge d'arxivat OpenRaster archivační obraz OpenRaster OpenRaster-arkivaftryk OpenRaster-Archivierungsbild Εικόνα αρχειοθέτησης OpenRaster OpenRaster archiving image imagen de archivado de OpenRaster OpenRaster artxiboaren irudia OpenRaster-arkistokuva OpenRaster goymslumynd image d'archive OpenRaster íomhá chartlannaithe OpenRaster imaxe arquivada de OpenRaster תמונת ארכיון של OpenRaster OpenRaster slika arhive OpenRaster archiválási kép Imagine de archivo OpenRaster Gambar pengarsipan OpenRaster Immagine archiviazione OpenRaster OpenRaster アーカイブイメージ OpenRaster-ის საარქივო გამოსახულება OpenRaster архивтеу суреті OpenRaster 압축 이미지 OpenRaster archyvavimo paveikslėlis OpenRaster arhivēšanas attēls OpenRaster archiverings-image imatge d'archiu OpenRaster Archiwalny obraz OpenRaster imagem arquivo OpenRaster Imagem de arquivamento OpenRaster Arhivă imagine OpenRaster архивное изображение OpenRaster Archivačný obrázok OpenRaster Odtis arhiva OpenRaster слика Опен Растер архивирања OpenRaster-arkivbild OpenRaster arşivleme görüntüsü архівоване зображення OpenRaster OpenRaster 归档映像 OpenRaster 封存影像 DirectDraw surface مساحة DirectDraw Pavierchnia DirectDraw Изображение — повърхност на DirectDraw superfície DirectDraw povrch DirectDraw DirectDraw-overflade DirectDraw-Oberfläche Επιφάνεια DirectDraw DirectDraw surface superficie de DirectDraw DirectDraw gainazala DirectDraw-piirtoalue DirectDraw yvirflata surface DirectDraw dromchla DirectDraw superficie de DirectDraw משטח של DirectDraw DirectDraw ploha DirectDraw felület Superficie DirectDraw Permukaan DirectDraw Superficie DirectDraw DirectDraw サーフェイス DirectDraw-ის ზედაპირი DirectDraw жазықтығы DirectDraw 서피스 DirectDraw paviršius DirectDraw virsma DirectDraw-overflate DirectDraw-oppervlak DirectDraw-overflate surfàcia DirectDraw Powierzchnia DirectDraw superfície DirectDraw Superfície do DirectDraw Suprafață DirectDraw плоскость DirectDraw Plocha DirectDraw Datoteka predmeta DirectDraw Superfaqe DirectDraw Директ Дров површина DirectDraw-yta DirectDraw yüzeyi поверхня DirectDraw Mặt DirectDraw DirectDraw 表面 DirectDraw 表面 X11 cursor مؤشر X11 Kursor X11 Курсор — X11 cursor X11 kurzor X11 X11-markør X11-Zeiger Δρομέας X11 X11 cursor cursor de X11 X11 kurtsorea X11-osoitin X11 vísi curseur X11 cúrsóir X11 Cursor X11 סמן של X11 X11 kursor X11 kurzor Cursor X11 Kursor X11 Cursore X11 X11 カーソル X11 курсоры X11 커서 X11 žymiklis X11 kursors X11-markør X11-muisaanwijzer X11-peikar cursor X11 Kursor X11 cursor X11 Cursor do X11 Cursor X11 курсор X11 Kurzor X11 Datoteka kazalke X11 Kursor X11 Икс11 курсор X11-muspekare X11 imleci курсор X11 Con chạy X11 X11 指针 X11 滑鼠游標 EXR image صورة EXR Vyjava EXR Изображение — EXR imatge EXR obrázek EXR EXR-billede EXR-Bild Εικόνα EXR EXR image EXR-bildo imagen EXR EXR irudia EXR-kuva EXR mynd image EXR íomhá EXR imaxe EXR תמונת EXR EXR slika EXR kép Imagine EXR Citra EXR Immagine EXR EXR 画像 EXR გამოსახულება EXR суреті EXR 그림 EXR paveikslėlis EXR attēls EXR-bilde EXR-afbeelding EXR-bilete imatge EXR Obraz EXR imagem EXR Imagem EXR Imagine EXR изображение EXR Obrázok EXR Slikovna datoteka EXR Figurë EXR ЕИксР слика EXR-bild EXR görüntüsü зображення EXR Ảnh EXR EXR 图像 EXR 影像 Macintosh Quickdraw/PICT drawing رسمة ماكنتوش Quickdraw/PICT Rysunak Macintosh Quickdraw/PICT Чертеж — Macintosh Quickdraw/PICT dibuix Quickdraw/PICT de Macintosh kresba Macintosh Quickdraw/PICT Macintosh Quickdraw/PICT-tegning Macintosh-Quickdraw/PICT-Zeichnung Σχέδιο Macintosh Quickdraw/PICT Macintosh Quickdraw/PICT drawing Quickdraw/PICT-grafikaĵo de Macintosh dibujo de Macintosh Quickdraw/PICT Macintosh Quickdraw/PICT marrazkia Macintosh Quickdraw/PICT -piirros Macintosh Quickdraw/PICT tekning dessin Macintosh Quickdraw/PICT líníocht Macintosh Quickdraw/PICT debuxo de Macintosh Quickdraw/PICT ציור של Macintosh Quickdraw/PICT Macintosh Quickdraw/PICT crtež Macintosh Quickdraw/PICT-rajz Designo QuickDraw/PICT de Macintosh Gambar Macintosh Quickdraw/PICT Disegno Macintosh Quickdraw/PICT Macintosh Quickdraw/PICT ドロー Macintosh Quickdraw/PICT суреті 매킨토시 Quickdraw/PICT 그림 Macintosh Quickdraw/PICT piešinys Macintosh Quickdraw/PICT zīmējums Lukisan Macintosh Quickdraw/PICT Macintosh Quickdraw/PICT-tegning Macintosh Quickdraw/PICT-tekening Macintosh Quickdraw/PICT-teikning dessenh Macintosh Quickdraw/PICT Rysunek QuickDraw/PICT Macintosh desenho Quickdraw/PICT de Macintosh Desenho do Macintosh Quickdraw/PICT Desen Macintosh Quickdraw/PICT рисунок Macintosh Quickdraw/PICT Kresba Macintosh QuickDraw/PICT Datoteka risbe Macintosh Quickdraw/PICT Vizatim Macintosh Quickdraw/PICT Мекинтошов Квикдров/ПИЦТ цртеж Macintosh Quickdraw/PICT-teckning Macintosh Quickdraw/PICT çizimi малюнок Macintosh Quickdraw/PICT Bản vẽ Quickdraw/PICT của Macintosh Macintosh Quickdraw/PICT 绘图 Macintosh Quickdraw/PICT 繪圖 UFRaw ID image صورة UFRaw ID Vyjava UFRaw ID Изображение — UFRaw ID imatge ID UFRaw obrázek ID UFRaw UFRaw ID-billede UFRaw-Bildbeschreibungsdatei Εικόνα UFRaw UFRaw ID image imagen de identificación UFRaw UFRaw ID irudia UFRaw ID -kuva UFRaw ID mynd image ID UFRaw íomhá aitheantais UFRaw imaxe de identificación UFRaw תמונה של UFRaw ID UFRaw ID slika UFRaw azonosítófájl Imagine UFRaw ID Citra UFRaw ID Immagine UFRaw ID UFRaw ID イメージ UFRaw ID суреті UFRaw ID 그림 UFRaw ID paveikslėlis UFRaw ID attēls UFRaw ID-bilde UFRaw ID-afbeelding UFRaw ID-bilete imatge ID UFRaw Obraz UFRaw ID imagem UFRaw ID Imagem ID do UFRaw ID imagine UFRaw изображение UFRaw ID Obrázok ID UFRaw Slikovna datoteka UFRaw ID Figurë UFRaw ID УФ сирова ИД слика UFRaw ID-bild UFRaw ID görüntüsü зображення UFRaw ID Ảnh ID UFRaw UFRaw ID 图像 UFRaw ID 影像 UFRaw Unidentified Flying Raw digital raw image صورة رقمية خامة suvoraja ličbavaja vyjava Изображение — digital raw imatge digital en cru digitální surový obrázek digitalt råbillede Digitales Rohbild Ανεπεξέργαστη ψηφιακή εικόνα digital raw image imagen digital en bruto irudi gordin digitala digitaalinen raakakuva talgild rámynd image brute numérique amhíomhá digiteach imaxe en bruto dixital תמונה דיגטלית גולמית Digitalna osnovna slika digitális nyers kép Imagine brute digital citra mentah digital Immagine raw digitale デジタル raw 画像 өңделмеген сандық суреттер 디지털 원본 사진 skaitmeninis neapdorotas paveikslėlis digitāls jēlattēls digitalt raw-bilde onbewerkt digitaal beeld digitalt råbilete imatge brut numeric Surowy obraz cyfrowy imagem digital em bruto Imagem digital bruta imagine digitală brută необработанные цифровые изображения Digitálny surový obrázok surova digitalna slika Figurë raw dixhitale дигитална сирова слика digital råbild sayısal ham görüntü зображення цифрового негатива ảnh thô số 数字化原始图像 數位原生影像 Adobe DNG negative Adobe DNG negative Adobe DNG Negative Изображение — Adobe DNG negative negatiu DNG d'Adobe negativ Adobe (DNG) Adobe DNG-negativ Adobe Digitales Negativ Αρνητικό Adobe DNG Adobe DNG negative negativo DNG de Adobe Adobe DNG negatiboa Adobe-DNG-negatiivi Adobe DNG negativ négatif DNG Adobe claonchló DNG Adobe negativo DNG de Adobe תשליל Adobe DNG Adobe DNG negativ Adobe DNG negatív Negativo Adobe DNG Negatif Adobe DNG Negativo Adobe DNG Adobe DNG ネガ Adobe DNG-ის ნეგატივი Adobe DNG негативі Adobe DNG 네거티브 Adobe DNG negatyvas Adobe DNG negatīvs Adobe DNG-negativ Adobe DNG-negatief Adobe DNG-negativ négatif DNG Adobe Negatyw DNG Adobe negativo Adobe DNG Negativo DNG da Adobe Negativ Adobe DNG негатив Adobe DNG Adobe Digital Negative (DNG) Datoteka negativa Adobe DNG Negativ Adobe DNG Адобов ДНГ негатив Adobe DNG-negativ Adobe DNG negatifi цифровий негатив DNG Adobe Âm bản Adobe DNG Adobe DNG 负片 Adobe DNG 負片 DNG Digital Negative Canon CRW raw image صورة Canon CRW خامة Suvoraja vyjava Canon CRW Изображение — Canon CRW raw imatge en cru de Canon CRW surový obrázek Canon CRW Canon CRW-råbillede Canon-CRW-Rohbild Ανεπεξέργαστη εικόνα Canon CRW Canon CRW raw image imagen en bruto CRW de Canon Canon CRW irudi gordina Canon-CRW-raakakuva Canon CRW rámynd image brute CRW Canon amhíomhá Canon CRW imaxe en bruto de Canon CRW תמונה גולמית של Canon CRW Canon CRW osnovna slika Canon CRW nyers kép Imagine brute CRW Canon Citra mentah Canon CRW Immagine raw Canon CRW Canon CRW raw 画像 Canon CRW raw გამოსახულება Canon CRW өңделмеген суреті 캐논 CRW RAW 사진 Canon CRW neapdorotas paveikslėlis Canon CRW jēlattēls Canon CRW raw-bilde onbewerkt Canon CRW-beeld Canon CRW råbilete imatge brut CRW Canon Surowy obraz CRW Canon imagem em bruto Canon CRW Imagem bruta CRW da Canon Imagine brută Canon CRW необработанное изображение Canon CRW Surový obrázok Canon CRW Surova slikovna datoteka Canon CRW Figurë raw Canon CRW Кенон ЦРВ сирова слика Canon CRW-råbild Canon CRW ham görüntüsü цифровий негатив CRW Canon Ảnh thô Canon CRW Canon CRW 原始图像 Canon CRW 原生影像 CRW Canon RaW Canon CR2 raw image صورة Canon CR2 خامة Suvoraja vyjava Canon CR2 Изображение — Canon CR2 raw imatge en cru de Canon CR2 surový obrázek Canon CR2 Canon CR2-råbillede Canon-CR2-Rohbild Ανεπεξέργαστη εικόνα Canon CR2 Canon CR2 raw image imagen en bruto CR2 de Canon Canon CR2 irudi gordina Canon-CR2-raakakuva Canon CR2 rámynd image brute CR2 Canon amhíomhá Canon CR2 imaxe en bruto de Canon CR2 תמונה גולמית של Canon CR2 Canon CR2 osnovna slika Canon CR2 nyers kép Imagine brute CR2 Canon Citra mentah Canon CR2 Immagine raw Canon CR2 Canon CR2 raw 画像 Canon CR2 raw გამოსახულება Canon CR2 өңделмеген суреті 캐논 CR2 RAW 사진 Canon CR2 neapdorotas paveikslėlis Canon CR2 jēlattēls Canon CR2 raw-bilde onbewerkt Canon CR2-beeld Canon CR2 råbilete imatge brut CR2 Canon Surowy obraz CR2 Canon imagem em bruto Canon CR2 Imagem bruta CR2 da Canon Imagine brută Canon CR2 необработанное изображение Canon CR2 Surový obrázok Canon CR2 Surova slikovna datoteka Canon CR2 Figurë raw Canon CR2 Кенон ЦР2 сирова слика Canon CR2-råbild Canon CR2 ham görüntüsü цифровий негатив CR2 Canon Ảnh thô Canon CR2 Canon CR2 原始图像 Canon CR2 原生影像 CR2 Canon Raw 2 Fuji RAF raw image صورة Fuji RAF خامة Suvoraja vyjava Fuji RAF Изображение — Fuji RAF raw imatge en cru de Fuji RAF surový obrázek Fuji RAF Fuji RAF-råbillede Fuji-RAF-Rohbild Ανεπεξέργαστη εικόνα Fuji RAF Fuji RAF raw image imagen en bruto RAF de Fuji Fuji RAF irudi gordina Fuji-RAF-raakakuva Fuji RAF raw mynd image brute RAF Fuji amhíomhá Fuji RAF imaxe en bruto de Fuji RAF תמונה גולמית של Fuji RAF Fuji RAF osnovna slika Fuji RAF nyers kép Imagine brute RAF de Fuji Citra mentah Fuji RAF Immagine raw Fuji RAF Fuji RAF raw 画像 Fuji RAF-ის raw გამოსახულება Fuji RAF өңделмеген суреті 후지 RAF RAW 사진 Fuji RAF neapdorotas paveikslėlis Fuji RAF jēlattēls Fuji RAF raw-bilde onbewerkt Fuji RAF-beeld Fuji RAF rått bilete imatge brut RAF Fuji Surowy obraz RAF Fuji imagem em bruto Fuji RAF Imagem bruta RAF da Fuji Imagine brută Fuji RAF необработанное изображение Fuji RAF Surový obrázok Fuji RAF Surova slikovna datoteka Fuji RAF Figurë raw Fuji RAF Фуџи РАФ сирова слика Fuji RAF-råbild Fuji RAF ham görüntüsü Цифровий негатив RAF Fuji Ảnh thô Fuji RAF 富士RAF 原始图像 Fuji RAF 原生影像 RAF RAw Format Kodak DCR raw image صورة Kodak DCR خامة Suvoraja vyjava Kodak DCR Изображение — Kodak DCR raw imatge en cru de Kodak DCR surový obrázek Kodak DCR Kodak DCR-råbillede Kodak-DCR-Rohbild Ανεπεξέργαστη εικόνα Kodak DCR Kodak DCR raw image imagen en bruto DCR de Kodak Kodak DCR irudi gordina Kodak-DCR-raakakuva Kodak DCR rámynd image brute DCR Kodak amhíomhá Kodak DCR imaxe en bruto de Kodad DCR תמונה גולמית של Kodak DCR Kodak DCR osnovna slika Kodak DCR nyers kép Imagine brute DCR de Kodak Citra mentah Kodak DCR Immagine raw Kodak DCR Kodak DCR raw 画像 Kodak DCR өңделмеген суреті 코닥 DCR RAW 사진 Kodak DCR neapdorotas paveikslėlis Kodak DCR jēlattēls Kodak DCR raw-bilde onbewerkt Kodak DCR-beeld Kodak DCR råbilete imatge brut DCR Kodak Surowy obraz DCR Kodak imagem em bruto Kodak DCR Imagem bruta DCR da Kodak Imagine brută Kodak DCR необработанное изображение Kodak DCR Surový obrázok Kodak DCR Surova slikovna datoteka Kodak DCR Figurë raw Kodak DCR Кодак ДЦР сирова слика Kodak DCR-råbild Kodak DCR ham görüntüsü цифровий негатив DCR Kodak Ảnh thô Kodak DCR Kodak DCR 原始图像 Kodak DCR 原生影像 DCR Digital Camera Raw Kodak K25 raw image صورة Kodak K25 خامة Suvoraja vyjava Kodak K25 Изображение — Kodak K25 raw imatge en cru de Kodak K25 surový obrázek Kodak K25 Kodak K25-råbillede Kodak-K25-Rohbild Ανεπεξέργαστη εικόνα Kodak K25 Kodak K25 raw image imagen en bruto K25 de Kodak Kodak K25 raw image Kodak-K25-raakakuva Kodak K25 rámynd image brute K25 Kodak amhíomhá Kodak K25 imaxe en bruto de Kodad K25 תמונה גולמית של Kodak K25 Kodak K25 osnovna slika Kodak K25 nyers kép Imagine brute K25 de Kodak Citra mentah Kodak K25 Immagine raw Kodak K25 Kodak K25 raw 画像 Kodak K25 өңделмеген суреті 코닥 K25 RAW 사진 Kodak K25 neapdorotas paveikslėlis Kodak K25 jēlattēls Kodak K25 raw-bilde onbewerkt Kodak K25-beeld Kodak K25 råbilete imatge brut K25 Kodak Surowy obraz K25 Kodak imagem em bruto Kodak K25 Imagem bruta K25 da Kodak Imagine brută Kodak K25 необработанное изображение Kodak K25 Surový obrázok Kodak K25 Surova slikovna datoteka Kodak K25 Figurë raw Kodak K25 Кодак К25 сирова слика Kodak K25-råbild Kodak K25 ham görüntüsü цифровий негатив K25 Kodak Ảnh thô Kodak K25 Kodak K25 原始图像 Kodak K25 原生影像 K25 Kodak DC25 Kodak KDC raw image صورة Kodak KDC خامة Suvoraja vyjava Kodak KDC Изображение — Kodak KDC raw imatge en cru de Kodak KDC surový obrázek Kodak KDC Kodak KDC-råbillede Kodak-KDC-Rohbild Ανεπεξέργαστη εικόνα Kodak KDC Kodak KDC raw image imagen en bruto KDC de Kodak Kodak KDC irudi gordina Kodak-KDC-raakakuva Kodak KDC rámynd image brute KDC Kodak amhíomhá Kodak KDC imaxe en bruto de Kodad KDC תמונה גולמית של Kodak KDC Kodak KDC osnovna slika Kodak KDC nyers kép Imagine brute KDC de Kodak Citra mentah Kodak KDC Immagine raw Kodak KDC Kodak KDC raw 画像 Kodak KDC өңделмеген суреті 코닥 KDC RAW 사진 Kodak KDC neapdorotas paveikslėlis Kodak KDC jēlattēls Kodak KDC raw-bilde onbewerkt Kodak KDC-beeld Kodak KDC råbilete imatge brut KDC Kodak Surowy obraz KDC Kodak imagem em bruto Kodak KDC Imagem bruta KDC da Kodak Imagine brută Kodak KDC необработанное изображение Kodak KDC Surový obrázok Kodak KDC Surova slikovna datoteka Kodak KDC Figurë raw Kodak KDC Кодак КДЦ сирова слика Kodak KDC-råbild Kodak KDC ham görüntüsü цифровий негатив KDC Kodak Ảnh thô Kodak KDC Kodak KDC 原始图像 Kodak KDC 原生影像 KDC Kodak Digital Camera Minolta MRW raw image صورة Minolta MRW خامة Suvoraja vyjava Minolta MRW Изображение — Minolta MRW raw imatge en cru de Minolta MRW surový obrázek Minolta MRW Minolta MRW-råbillede Minolta-MRW-Rohbild Ανεπεξέργαστη εικόνα Minolta MRW Minolta MRW raw image imagen en bruto MRW de Minolta Minolta MRW irudi gordina Minolta-MRW-raakakuva Minolta MRW rámynd image brute MRW Minolta amhíomhá Minolta MRW imaxe RAW de Minolta MRW תמונה גולמית של Minolta MRW Minolta MRW osnovna slika Minolta MRW nyers kép Imagine brute Minolta MRW Citra mentah Minolta MRW Immagine raw Minolta MRW Minolta MRW raw 画像 Minolta MRW өңделмеген суреті 미놀타 MRW RAW 사진 Minolta MRW neapdorotas paveikslėlis Minolta MRW jēlattēls Minolta MRW raw-bilde onbewerkt Minolta MRW-beeld Minolta MRW råbilete imatge brut MRW Minolta Surowy obraz MRW Minolta imagem em bruto Minolta MRW Imagem bruta MRW do Minolta Imagine brută Minolta MRW необработанное изображение Minolta MRW Surový obrázok Minolta MRW Surova slikovna datoteka Minolta MRW Figurë raw Minolta MRW Минолта МРВ сирова слика Minolta MRW-råbild Minolta MRW ham görüntüsü цифровий негатив MRW Minolta Ảnh thô Minolta MRW Minolta MRW 原始图像 Minolta MRW 原生影像 MRW Minolta RaW Nikon NEF raw image صورة Nikon NEF خامة Suvoraja vyjava Nikon NEF Изображение — Nikon NEF raw imatge en cru de Nikon NEF surový obrázek Nikon NEF Nikon NEF-råbillede Nikon-NEF-Rohbild Ανεπεξέργαστη εικόνα Nikon NEF Nikon NEF raw image imagen en bruto NEF de Nikon Nikon NEF irudi gordina Nikon-NEF-raakakuva Nikon NEF rámynd image brute NEF Nikon amhíomhá Nikon NEF imaxe RAW NEF Nikon תמונה גולמית של Nikon NEF Nikon NEF osnovna slika Nikon NEF nyers kép Imagine brute Nikon NEF Citra mentah Nikon NEF Immagine raw Nikon NEF Nikon NEF raw イメージ Nikon NEF өңделмеген суреті 니콘 NEF RAW 사진 Nikon NEF neapdorotas paveikslėlis Nikon NEF jēlattēls Nikon NEF raw-bilde onbewerkt Nikon NEF-beeld Nikon NEF råbilete imatge brut NEF Nikon Surowy obraz NEF Nikon imagem em bruto Nikon NEF Imagem bruta NEF da Nikon Imagine brută Nikon NEF необработанное изображение Nikon NEF Surový obrázok Nikon NEF Surova slikovna datoteka Nikon NEF Figurë raw Nikon NEF Никон НЕФ сирова слика Nikon NEF-råbild Nikon NEF ham görüntüsü цифровий негатив NEF Nikon Ảnh thô Nikon NEF Nikon NEF 原始图像 Nikon NEF 原生影像 NEF Nikon Electronic Format Olympus ORF raw image صورة Olympus ORF خامة Suvoraja vyjava Olympus ORF Изображение — Olympus ORF raw imatge en cru d'Olympus ORF surový obrázek Olympus ORF Olympus ORF-råbillede Olympus-ORF-Rohbild Ανεπεξέργαστη εικόνα Olympus ORF Olympus ORF raw image imagen en bruto ORF de Olympus Olympus ORF irudi gordina Olympus-ORF-raakakuva Olympus ORF rámynd image brute ORF Olympus amhíomhá Olympus ORF imaxe en bruto de Olympus ORF תמונה גולמית של Olympus ORF Olympus ORF osnovna slika Olympus ORF nyers kép Imagine brute Olympus ORF Citra mentah Olympus ORF Immagine raw Olympus ORF Olympus ORF raw 画像 Olympus ORF-ის raw გამოსახულება Olympus ORF өңделмеген суреті 올림푸스 ORF RAW 사진 Olympus ORF neapdorotas paveikslėlis Olympus ORF jēlattēls Olympus ORF raw-bilde onbewerkt Olympus ORF-beeld Olympus ORF råbilete imatge brut ORF Olympus Surowy obraz Olympus ORF imagem em bruto Olympus ORF Imagem bruta ORF da Olympus Imagine brută Olympus ORF необработанное изображение Olympus ORF Surový obrázok Olympus ORF Surova slikovna datoteka Olympus ORF Figurë raw Olympus ORF Олимпус ОРФ сирова слика Olympus ORF-råbild Olympus ORF ham görüntüsü цифровий негатив ORF Olympus Ảnh thô Olympus ORF Olympus ORF 原始图像 Olympus ORF 原生影像 ORF Olympus Raw Format Panasonic raw image صورة Panasonic خامة Suvoraja vyjava Panasonic Изображение — Panasonic raw imatge en cru de Panasonic surový obrázek Panasonic Panasonicråbillede (raw) Panasonic-Rohbild Ανεπεξέργαστη εικόνα Panasonic Panasonic raw image imagen en bruto de Panasonic Panasonic irudi gordina Panasonic-raakakuva Panasonic rámynd image brute Panasonic amhíomhá Panasonic imaxe en bruto de Panasonic תמונה גולמית של Panasonic Panasonic osnovna slika Panasonic nyers kép Imagine brute Panasonic Citra mentah Panasonic Immagine raw Panasonic Panasonic raw 画像 Panasonic өңделмеген суреті 파나소닉 RAW 사진 Panasonic neapdorotas paveikslėlis Panasonic jēlattēls Panasonic raw-bilde onbewerkt Panasonic-beeld Panasonic råbilete imatge brut Panasonic Obraz raw Panasonic imagem em bruto Panasonic Imagem bruta da Panasonic Imagine brută Panasonic необработанное изображение Panasonic Surový obrázok Panasonic Surova slikovna datoteka Panasonic Figurë raw Panasonic Панасоник сирова слика Panasonic-råbild Panasonic ham görüntüsü цифровий негатив Panasonic Ảnh thô Panasonic Panasonic 原始图像 Panasonic 原生影像 Panasonic raw2 image Изображение — Panasonic raw2 imatge «RAW2» de Panasonic surový obrázek Panasonic raw2 Panasonic-rå2-billede (raw) Panasonic raw2-Bild Ανεπεξέργαστη εικόνα Panasonic (raw2) Panasonic raw2 image imagen en bruto raw2 de Panasonic Panasonic raw2 irudia Panasonic raw2 -kuva image raw2 Panasonic imaxe en bruto raw2 de Panasonic תמונת raw2 של Panasonic Panasonic raw2 image Panasonic raw2 kép Imagine raw2 Panasonic Image Panasonic raw2 Immagine raw2 Panasonic Panasonic raw2 画像 Panasonic raw2 суреті 파나소닉 RAW2 사진 Panasonic raw2 jēlattēls Panasonic raw2 image imatge raw2 Panasonic Obraz raw2 Panasonic imagem em bruto Panasonic Imagem raw2 da Panasonic необработанное изображение Panasonic RAW 2 Surový obrázok Panasonic raw2 Slikovna datoteka Panasonic raw2 Панасоник сирова2 слика Panasonic raw2-bild Panasonic raw2 görüntüsü зображення формату raw2 Panasonic Panasonic raw2 图像 Panasonic raw2 影像 Pentax PEF raw image صورة Pentax PEF خامة Suvoraja vyjava Pentax PEF Изображение — Pentax PEF raw imatge en cru de Pentax PEF surový obrázek Pentax PEF Pentax PEF-råbillede Pentax-PEF-Rohbild Ανεπεξέργαστη εικόνα Pentax PEF Pentax PEF raw image imagen en bruto PEF de Pentax Pentax PEF irudi gordina Pentax-PEF-raakakuva Pentax PEF rámynd image brute PEF Pentax amhíomhá Pentax PEF imaxe en bruto PEF de Pentax תמונה גולמית של Pentax PEF Pentax PEF osnovna slika Pentax PEF nyers kép Imagine brute Pentax PEF Citra mentah Pentax PEF Immagine raw Pentax PEF Pentax PEF raw 画像 Pentax PEF өңделмеген суреті 펜탁스 PEF RAW 사진 Pentax PEF neapdorotas paveikslėlis Pentax PEF jēlattēls Pentax PEF raw-bilde onbewerkt Pentax PEF-beeld Pentax PEF råbilete imatge brut PEF Pentax Surowy obraz Pentax PEF imagem em bruto Pentax PEF Imagem bruta PEF da Pentax Imagine brută Pentax PEF необработанное изображение Pentax PEF Surový obrázok Pentax PEF Surova slikovna datoteka Pentax PEF Figurë raw Pentax PEF Пентакс ПЕФ сирова слика Pentax PEF-råbild Pentax PEF ham görüntüsü цифровий негатив PEF Pentax Ảnh thô Pentax PEF Pentax PEF 原始图像 Pentax PEF 原生影像 PEF Pentax Electronic Format Sigma X3F raw image صورة Sigma X3F خامة Suvoraja vyjava Sigma X3F Изображение — Sigma X3F raw imatge en cru de Sigma X3F surový obrázek Sigma X3F Sigma X3F-råbillede Sigma-X3F-Rohbild Ανεπεξέργαστη εικόνα Sigma X3F Sigma X3F raw image imagen en bruto X3F de Sigma Sigma X3F irudi gordina Sigma-X3F-raakakuva Sigma X3F rámynd image brute X3F Sigma amhíomhá Sigma X3F imaxe en bruto X3F de Sigma תמונה גולמית של Sigma X3F Sigma X3F osnovna slika Sigma XF3 nyers kép Imagine brute Sigma X3F Citra mentah Sigma X3F Immagine raw Sigma X3F Sigma X3F raw 画像 Sigma X3F өңделмеген суреті 시그마 X3F RAW 사진 Sigma X3F neapdorotas paveikslėlis Sigma X3F jēlattēls Sigma X3F raw-bilde onbewerkt Sigma X3F-beeld Sigma X3F råbilete imatge brut X3F Sigma Surowy obraz X3F Sigma imagem em bruto Sigma X3F Imagem bruta X3F da Sigma Imagine brută Sigma X3F необработанное изображение Sigma X3F Surový obrázok Sigma X3F Surova slikovna datoteka Sigma X3F Fifurë raw Sigma X3F Сигма Икс3Ф сирова слика Sigma X3F-råbild Sigma X3F ham görüntüsü цифровий негатив X3F Sigma Ảnh thô Sigma X3F Sigma X3F 原始图像 Sigma X3F 原生影像 X3F X3 Foveon Sony SRF raw image صورة Sony SRF خامة Suvoraja vyjava Sony SRF Изображение — Sony SRF raw imatge en cru de Sony SRF surový obrázek Sony SRF Sony SRF-råbillede Sony-SRF-Rohbild Ανεπεξέργαστη εικόνα Sony SRF Sony SRF raw image imagen en bruto SRF de Sony Sony SRF irudi gordina Sony-SRF-raakakuva Sony SRF rámynd image brute SRF Sony amhíomhá Sony SRF imaxe en bruto SRF de sony תמונה גולמית של Sony SRF Sony SRF osnovna slika Sony SRF nyers kép Imagine brute Sony SRF Citra mentah Sony SRF Immagine raw Sony SRF Sony SRF raw 画像 Sony SRF өңделмеген суреті 소니 SRF RAW 사진 Sony SRF neapdorotas paveikslėlis Sony SRF jēlattēls Sony SRF raw-bilde onbewerkt Sony SRF-beeld Sony SRF råbilete imatge brut SRF Sony Surowy obraz SRF Sony imagem em bruto Sony SRF Imagem bruta SRF da Sony Imagine brută Sony SRF необработанное изображение Sony SRF Surový obrázok Sony SRF Surova slikovna datoteka Sony SRF Figurë raw Sony SRF Сони СРФ сирова слика Sony SRF-råbild Sony SRF ham görüntüsü цифровий негатив SRF Sony Ảnh thô Sony SRF Sony SRF 原始映像 Sony SRF 原生影像 SRF Sony Raw Format Sony SR2 raw image صورة Sony SR2 خامة Suvoraja vyjava Sony SR2 Изображение — Sony SR2 raw imatge en cru de Sony SR2 surový obrázek Sony SR2 Sony SR2-råbillede Sony-SR2-Rohbild Ανεπεξέργαστη εικόνα Sony SR2 Sony SR2 raw image imagen en bruto SR2 de Sony Sony SR2 irudi gordina Sony-SR2-raakakuva Sony SR2 rámynd image brute SR2 Sony amhíomhá Sony SR2 imaxe en bruto SR2 de sony תמונה גולמית של Sony SR2 Sony SR2 osnovna slika Sony SR2 nyers kép Imagine brute Sony SR2 Citra mentah Sony SR2 Immagine raw Sony SR2 Sony SR2 raw 画像 Sony SR2 өңделмеген суреті 소니 SR2 RAW 사진 Sony SR2 neapdorotas paveikslėlis Sony SR2 jēlattēls Sony SR2 raw-bilde onbewerkt Sony SR2-beeld Sony SR2 råbilete imatge brut SR2 Sony Surowy obraz SR2 Sony imagem em bruto Sony SR2 Imagem bruta SR2 da Sony Imagine brută Sony SR2 необработанное изображение Sony SR2 Surový obrázok Sony SR2 Surova slikovna datoteka Sony SR2 Figurë raw Sony SR2 Сони СР2 сирова слика Sony SR2-råbild Sony SR2 ham görüntüsü цифровий негатив SR2 Sony Ảnh thô Sony SR2 Sony SR2 原始映像 Sony SR2 原生影像 SR2 Sony Raw format 2 Sony ARW raw image صورة Sony ARW خامة Suvoraja vyjava Sony ARW Изображение — Sony ARW raw imatge en cru de Sony ARW surový obrázek Sony ARW Sony ARW-råbillede Sony-ARW-Rohbild Ανεπεξέργαστη εικόνα Sony ARW Sony ARW raw image imagen en bruto ARW de Sony Sony ARW irudi gordina Sony-ARW-raakakuva Sony ARW rámynd image brute ARW Sony amhíomhá Sony ARW imaxe en bruto ARW de sony תמונה גולמית של Sony ARW Sony ARW osnovna slika Sony ARW nyers kép Imagine brute Sony ARW Citra mentah Sony ARW Immagine raw Sony ARW Sony ARW raw 画像 Sony ARW өңделмеген суреті 소니 ARW RAW 사진 Sony ARW neapdorotas paveikslėlis Sony ARW jēlattēls Sony ARW raw-bilde onbewerkt Sony ARW-beeld Sony ARW råbilete imatge brut ARW Sony Surowy obraz ARW Sony imagem em bruto Sony ARW Imagem bruta ARW da Sony Imagine brută Sony ARW необработанное изображение Sony ARW Surový obrázok Sony ARW Surova slikovna datoteka Sony ARW Figurë raw Sony ARW Сони АРВ сирова слика Sony ARW-råbild Sony ARW ham görüntüsü цифровий негатив ARW Sony Ảnh thô Sony ARW Sony ARW 原始映像 Sony ARW 原生影像 ARW Alpha Raw format PNG image صورة PNG PNG rəsmi Vyjava PNG Изображение — PNG imatge PNG obrázek PNG Delwedd PNG PNG-billede PNG-Bild Εικόνα PNG PNG image PNG-bildo imagen PNG PNG irudia PNG-kuva PNG mynd image PNG íomhá PNG imaxe PNG תמונת PNG PNG slika PNG-kép Imagine PNG Citra PNG Immagine PNG PNG 画像 PNG суреті PNG 그림 PNG paveikslėlis PNG attēls Imej PNG PNG-bilde PNG-afbeelding PNG-bilete imatge PNG Obraz PNG imagem PNG Imagem PNG Imagine PNG изображение PNG Obrázok PNG Slikovna datoteka PNG Figurë PNG ПНГ слика PNG-bild PNG görüntüsü зображення PNG Ảnh PNG PNG 图像 PNG 影像 Run Length Encoded bitmap image تشغيل صورة نقطية طولية الترميز Bitmapnaja vyjava, zakadavanaja ŭ Run Length Изображение — RLE Bitmap imatge de mapa de bits «Run Lenght Encoded» obrázek bitové mapy Run Length Encoded Run Length Encoded-bitmapbillede Lauflängenkodiertes Bitmap-Bild Εικόνα bitmap κωδικοποιημένου μήκος εκτέλεσης Run Length Encoded bitmap image mapa de bits con codificación del tamaño durante la ejecución 'Run Lenght Encoded' bitmap irudia RLE-koodattu bittikartta image matricielle Run Length Encoded íomhá mhapa giotáin Run Length Encoded mapa de bits con codificación do tamaño durante a execución מקודד מפת סיביות של Run Length Run Length Encoded bitmap slika Run Length Encoded bitkép Imagine raster in codification Run-Length Citra peta bit Run Length Encoded Immagine bitmap RLE (Run Length Encoded) ランレングス符号化ビットマップ画像 RLE сығылған растрлік суреті RLE 인코딩된 비트맵 그림 Run Length Encoded rastrinis paveikslėlis Secīgo atkārtojumu kodēts bitkartes attēls Run Length Encoded bitmap bilde RLE-gecodeerde bitmap-afbeelding Run Length Encoded punktgrafikk imatge matriciala Run Length Encoded Obraz bitmapy RLE mapa de bitas Run Length Encoded Classe de comprimento imagem bitmap codificada Imagine bitmap codată RLE растровое изображение (сжатое RLE) Bitmapový obrázok Run Length Encoded Zaporedno kodirana bitna slika (RLE) Figurë bitmap RLE (Run Length Encoded) битмап слика кодирана дужином скупине Körlängdskodad bitmappbild Run Length Encoded bit eşlem görüntüsü растрове зображення RLE Ảnh mảng mã hóa chiều dài chạy (RLE) 游程编码位图 Run Length Encoded 點陣影像 SVG image صورة SVG Vyjava SVG Изображение — SVG imatge SVG obrázek SVG SVG-billede SVG-Bild Εικόνα SVG SVG image SVG-bildo imagen SVG SVG irudia SVG-kuva SVG mynd image SVG íomhá SVG imaxe SVG תמונת SVG SVG slika SVG kép Imagine SVG Citra SVG Immagine SVG SVG 画像 SVG суреті SVG 그림 SVG paveikslėlis SVG attēls SVG-bilde SVG-afbeelding SVG-bilete imatge SVG Obraz SVG imagem SVG Imagem SVG Imagine SVG изображение SVG Obrázok SVG Slikovna vektorska datoteka SVG Figurë SVG СВГ слика SVG-bild SVG görüntüsü зображення SVG Ảnh SVG SVG 图像 SVG 影像 SVG Scalable Vector Graphics compressed SVG image صورة SVG مضغوطة skampresavanaja vyjava SVG Изображение — SVG, компресирано imatge SVG amb compressió komprimovaný obrázek SVG SVG-komprimeret billede Komprimiertes SVG-Bild Συμπιεσμένη εικόνα SVG compressed SVG image imagen SVG comprimida konprimitutako SVG irudia pakattu SVG-kuva stappað SVG mynd image SVG compressée íomhá SVG comhbhrúite imaxe SVG comprimida תמונת SVG מכווצת komprimirana SVG slika tömörített SVG kép Imagine SVG comprimite Citra SVG terkompresi Immagine SVG compressa 圧縮 SVG 画像 сығылған SVG суреті 압축된 SVG 그림 suglaudintas SVG paveikslėlis saspiests SVG attēls komprimert SVG-bilde ingepakte SVG-afbeelding komprimert SVG-bilete imatge SVG compressat Skompresowany obraz SVG imagem SVG comprimida Imagem SVG compactada imagine comprimată SVG сжатое изображение SVG Komprimovaný obrázok SVG Slikovna datoteka SVG (stisnjena) Figurë SVG e kompresuar запакована СВГ слика komprimerad SVG-bild sıkıştırılmış SVG görüntüsü стиснене зображення SVG ảnh SVG đã nén 压缩的 SVG 图像 壓縮版 SVG 影像 SVG Scalable Vector Graphics TIFF image صورة TIFF Vyjava TIFF Изображение — TIFF imatge TIFF obrázek TIFF TIFF-billede TIFF-Bild Εικόνα TIFF TIFF image TIFF-bildo imagen TIFF TIFF irudia TIFF-kuva TIFF mynd image TIFF íomhá TIFF imaxe TIFF תמונת TIFF TIFF slika TIFF-kép Imagine TIFF Citra TIFF Immagine TIFF TIFF 画像 TIFF суреті TIFF 그림 TIFF paveikslėlis TIFF attēls Imej TIFF TIFF-bilde TIFF-afbeelding TIFF-bilete imatge TIFF Obraz TIFF imagem TIFF Imagem TIFF Imagine TIFF изображение TIFF Obrázok TIFF Slikovna datoteka TIFF Figurë TIFF ТИФФ слика TIFF-bild TIFF görüntüsü зображення TIFF Ảnh TIFF TIFF 图像 TIFF 影像 TIFF Tagged Image File Format Multi-page TIFF image imatge TIFF multipàgina vícestránkový obrázek TIFF Flersidet TIFF-billede Mehrseitiges TIFF-Bild Πολυσέλιδη εικόνα TIFF Multi-page TIFF image imagen TIFF de varias páginas Orri anitzeko TIFF irudia Monisivuinen TIFF-kuva Image TIFF multi-page Imaxe TIFF multipáxina תמונת TIFF עם ריבוי עמודים Višestrana TIFF slika Többoldalas TIFF kép Imagine TIFF multi-pagina Citra TIFF multi halaman Immagine TIFF multi-pagina Көпбеттік TIFF суреті 다중 페이지 TIFF 그림 Imatge TIFF multipagina Wielostronnicowy obraz TIFF imagem TIFF multipágina Imagem TIFF multipágina Многостраничное изображение TIFF Viacstránkový obrázok TIFF Večstranska slika TIFF Вишестранична ТИФФ слика Flersidig TIFF-bild Çok sayfalı TIFF görüntüsü багатосторінкове зображення TIFF 多页 TIFF 图像 多頁 TIFF 影像 TIFF Tagged Image File Format AutoCAD image صورة AutoCAD AutoCAD rəsmi Vyjava AutoCAD Изображение — AutoCAD imatge d'AutoCAD obrázek AutoCAD Delwedd AutoCAD AutoCAD-billede AutoCAD-Bild Εικόνα AutoCAD AutoCAD image AutoCAD-bildo imagen de AutoCAD AutoCAD-eko irudia AutoCAD-kuva AutoCAD mynd image AutoCAD íomhá AutoCAD imaxe de AutoCAD תמונה של AutoCAD AutoCAD slika AutoCAD-kép Imagine AutoCAD Citra AutoCAD Immagine AutoCAD AutoCAD 画像 AutoCAD-ის გამოსახულება AutoCAD суреті AutoCAD 그림 AutoCAD paveikslėlis AutoCAD attēls Imej AutoCAD AutoCAD-bilde AutoCAD-afbeelding AutoCAD-bilete imatge AutoCAD Obraz AutoCAD imagem AutoCAD Imagem do AutoCAD Imagine AutoCAD изображение AutoCAD Obrázok AutoCAD Slikovna datoteka AutoCAD Figurë AutoCAD АутоКАД слика AutoCAD-bild AutoCAD görüntüsü зображення AutoCAD Ảnh AutoCAD AutoCAD 图像 AutoCAD 影像 DXF vector image صورة DXF نقطية Vektarnaja vyjava DXF Изображение — DXF imatge vectorial DXF vektorový obrázek DXF DXF-vektorbillede DXF-Vektorbild Διανυσματική εικόνα DXF DXF vector image vektora DXF-bildo imagen vectorial DXF DXF bektore-grafikoa DXF-vektorikuva DXF vektormynd image vectorielle DXF íomhá veicteoir DXF imaxe de vector DXF תמונת DXF וקטורית DXF vektorska slika DXF-vektorkép Imagine vectorial DXF Citra vektor DXF Immagine vettoriale DXF DXF ベクター画像 DXF ვექტორული გამოსახულება DXF векторлық суреті DXF 벡터 그림 DXF vektorinis paveikslėlis DXF vektora attēls Imej vektor DXF DXF-vektorgrafikk DXF-vectorafbeelding DXF-vektorgrafikk imatge vectorial DXF Obraz wektorowy DXF imagem de vectores DXF Imagem vetorial DXF Imagine vectorială DXF векторное изображение DXF Vektorový obrázok DXF Slikovna vektorska datoteka DXF Figurë vektoriale DFX ДИксФ векторска графика DXF-vektorbild DXF vektör görüntüsü векторне зображення DXF Ảnh véc-tơ DXF DXF 矢量图像 DXF 向量圖 Microsoft Document Imaging format صيغة مستند تصوير مايكروسوفت Изображение — Microsoft Document Imaging format Microsoft Document Imaging formát Microsoft Document Imaging Microsofts dokumentbilledformat Microsoft-Document-Imaging-Bildformat Μορφή Microsoft Document Imaging Microsoft Document Imaging format formato de imagen para documentos de Microsoft Microsoft Document Imaging formatua Microsoft Document Imaging -muoto Microsoft Document Imaging snið format Document Imaging Microsoft formáid Microsoft Document Imaging formato de Microsoft Document Imaging תבנית של Microsoft Document Imaging Microsoft Document Imaging format Microsoft Document Imaging formátum File in formato Microsoft Document Imaging Format Microsoft Document Imaging Formato MDI (Microsoft Document Imaging) Microsoft ドキュメントイメージフォーマット Microsoft Document Imaging пішімі Microsoft 문서 이미지 형식 Microsoft Document Imaging formatas Microsoft dokumentu attēlošanas formāts Microsoft Document Imaging format Document Imaging Microsoft Format Microsoft Document Imaging formato Microsoft Document Imaging Formato do Microsoft Document Imaging Format Microsoft Document Imaging формат Microsoft Document Imaging Formát Microsoft Document Imaging Zapis Microsoft Document Imaging запис слика Мајкрософтовог документа Microsoft Document Imaging-format Microsoft Belge Görüntüleme biçimi формат Microsoft Document Imaging Định dạng tạo ảnh tài liệu Microsoft Microsoft Document Imaging 扫描图像 微軟文件影像格式 MDI Microsoft Document Imaging WebP image imatge WebP obrázek WebP WebP-billede WebP-Bild Εικόνα WebP WebP image imagen WebP WebP irudia WebP-kuva image WebP Imaxe WebP תמונת WebP WebP slika WebP kép Imagine WebP Citra WebP Immagine WebP WebP суреті WebP 그림 imatge WebP Obraz WebP imagem WebP Imagem WebP Изображение WebP Obrázok WebP Slika WebP ВебП слика WebP-bild WebP görüntüsü зображення WebP WebP 图像 WebP 影像 3D Studio image صورة استديو ثلاثية الأبعاد 3D Studio rəsmi Vyjava 3D Studio Изображение — 3D Studio imatge de 3D Studio obrázek 3D Studio Delwedd "3D Studio" 3D Studio-billede 3D-Studio-Bild Εικόνα 3D Studio 3D Studio image bildo de 3D Studio imagen de 3D Studio 3D Studio-ko irudia 3D Studio -kuva 3D Studio mynd image 3D Studio íomhá 3D Studio Imaxe de 3D Studio תמונת 3D Studio 3D Studio slika 3D Studio-kép Imagine 3D Studio Citra 3D Studio Immagine 3D Studio 3D Studio 画像 3D Studio-ის გამოსახულება 3D Studio суреті 3D Studio 그림 3D Studio paveikslėlis 3D Studio attēls Imej 3D Studio 3D Studio-bilde 3D-Studio-afbeelding 3D Studio-bilete imatge 3D Studio Obraz 3D Studio imagem 3D Studio Imagem do 3D Studio Imagine 3D Studio сцена 3D Studio Obrázok 3D Studio Slikovna datoteka 3D Studio Figurë 3D Studio слика 3Д Студија 3D Studio-bild 3D Studio görüntüsü зображення 3D Studio Ảnh xuởng vẽ 3D 3D Studio 图像 3D Studio 影像 Applix Graphics image صورة رسوميات Applix Vyjava Applix Graphics Изображение — Applix Graphics imatge d'Applix Graphics obrázek Applix Graphics Applix Graphics-billede Applix-Graphics-Bild Εικόνα Applix Graphics Applix Graphics image bildo de Applix Graphics imagen de Applix Graphics Applix Graphics irudia Applix Graphics -kuva Applix Graphics mynd image Applix Graphics íomhá Applix Graphics imaxe de Applix Graphics תמונה של Applix Graphics Applix Graphics slika Applix Graphics-kép Imagine Applix Graphics Citra Applix Graphics Immagine Applix Graphics Applix Graphics 画像 Applix Graphics-ის გამოსახულება Applix Graphics суреті Applix Graphics 그림 Applix Graphics paveikslėlis Applix Graphics attēls Imej Applix Graphics Applix Graphics-dokument Applix Graphics-afbeelding Applix Graphics-dokument imatge Applix Graphics Obraz Applix Graphics imagem Applix Graphics Imagem do Applix Graphics Imagine Applix Graphics изображение Applix Graphics Obrázok Applix Graphics Slikovna datoteka Applix Graphics Figurë Applix Graphics Апликсов графички документ Applix Graphics-bild Applix Graphics görüntüsü зображення Applix Graphics Ảnh Applix Graphics Applix Graphics 图像 Applix Graphics 影像 EPS image (bzip-compressed) صورة EPS (مضغوط-bzip) Vyjava EPS (bzip-skampresavanaja) Изображение — EPS, компресирано с bzip imatge EPS (amb compressió bzip) obrázek EPS (komprimovaný pomocí bzip) EPS-billede (bzip-komprimeret) EPS-Bild (bzip-komprimiert) Εικόνα EPS (συμπιεσμένη bzip) EPS image (bzip-compressed) imagen EPS (comprimida con bzip) EPS irudia (bzip-ekin konprimitua) EPS-kuva (bzip-pakattu) EPS mynd (bzip-stappað) image EPS (compressée bzip) íomhá EPS (comhbhrúite le bzip) imaxe EPS (comprimida con bzip) תמונת EPS (מכווץ בbzip) EPS slika (komprimirana bzip-om) EPS kép (bzip-tömörítésű) Imagine EPS (comprimite con bzip) Citra EPS (terkompresi bzip) Immagine EPS (compressa con bzip) EPS 画像 (bzip 圧縮) EPS გამოსახულება (bzip-ით შეკუმშული) EPS суреті (bzip-пен сығылған) EPS 그림(BZIP 압축) EPS paveikslėlis (suglaudintas su bzip) EPS attēls (saspiests ar bzip) EPS-bilde (bzip-komprimert) EPS-afbeelding (ingepakt met bzip) EPS-bilete (pakka med bzip) imatge EPS (compressat bzip) Obraz EPS (kompresja bzip) imagem EPS (compressão bzip) Imagem EPS (compactada com bzip) Imagine EPS (compresie bzip) изображение EPS (сжатое bzip) Obrázok EPS (komprimovaný pomocou bzip) Slikovna datoteka EPS (stisnjena z bzip) Figurë EPS (e kompresuar me bzip) ЕПС слика (запакована бзип-ом) EPS-bild (bzip-komprimerad) EPS görüntüsü (bzip ile sıkıştırılmış) зображення EPS (стиснене bzip) Ảnh EPS (đã nén bzip) EPS 图像(bzip 压缩) EPS 影像 (bzip 格式壓縮) CMU raster image صورة CMU نقطية CMU raster rəsmi Rastravaja vyjava CMU Изображение — CMU raster imatge ràster CMU rastrový obrázek CMU Delwedd raster CMU CMU-rasterbillede CMU-Rasterbild Εικόνα ράστερ CMU CMU raster image rastruma bildo de CMU imagen ráster CMU CMU bilbe-irudia CMU-rasterikuva CMU raster mynd image raster CMU íomhá rastar CMU imaxe raster CMU תמונת סריקה CMU CMU iscrtana slika CMU-raszterkép Imagine raster CMU Citra raster CMU Immagine raster CMU CMU ラスター画像 CMU-ის რასტრული გამოსახულება CMU растрлық суреті CMU 래스터 그림 CMU rastrinis paveikslėlis CMU rastra attēls Imej raster CMU CMU-rasterbilde CMU-rasterafbeelding CMU rasterbilete imatge raster CMU Obraz rastrowy CMU imagem raster CMU Imagem raster CMU Imagine raster CMU растровое изображение CMU Rastrový obrázok CMU Slikovna rastrska datoteka CMU Figurë raster CMU ЦМУ растерска слика CMU-rasterbild CMU tarama görüntüsü растрове зображення CMU Ảnh mành CMU CMU 矢量图像 CMU raster 影像 compressed GIMP image صورة GIMP مضغوطة skampresavanaja vyjava GIMP Изображение — GIMP, компресирано imatge GIMP amb compressió komprimovaný obrázek GIMP komprimeret GIMP-billede Komprimiertes GIMP-Bild Συμπιεσμένη εικόνα GIMP compressed GIMP image imagen GIMP comprimida konprimitutako GIMP irudia pakattu GIMP-kuva stappað GIMP mynd image GIMP compressée íomhá GIMP comhbhrúite imaxe de GIMP comprimida תמונת GIMP מכווצת Sažeta GIMP slika tömörített GIMP kép Imagine GIMP comprimite Citra GIMP terkompresi Immagine GIMP compressa 圧縮 GIMP 画像 сығылған GIMP суреті 압축된 GIMP 그림 suglaudintas GIMP paveikslėlis saspiests GIMP attēls komprimert GIMP-bilde ingepakte GIMP-afbeelding komprimert GIMP-bilete imatge GIMP compressat Skompresowany obraz GIMP imagem GIMP comprimida Imagem do GIMP compactada imagine comprimată GIMP сжатое изображение GIMP Komprimovaný obrázok GIMP Slikovna datoteka GIMP (stisnjena) Figurë GIMP e kompresuar запакована ГИМП слика komprimerad GIMP-bild sıkıştırılmış GIMP görüntüsü стиснене зображення GIMP ảnh GIMP đã nén 压缩的 GIMP 图像 壓縮版 GIMP 影像 DICOM image صورة DICOM Vyjava DICOM Изображение — DICOM imatge DICOM obrázek DICOM DICOM-billede DICOM-Bild Εικόνα DICOM DICOM image DICOM-bildo imagen DICOM DICOM irudia DICOM-kuva DICOM mynd image DICOM íomhá DICOM imaxe DICOM תמונת DICOM DICOM slika DICOM kép Imagine DICOM Citra DICOM Immagine DICOM DICOM 画像 DICOM გამოსახულება DICOM суреті DICOM 그림 DICOM paveikslėlis DICOM attēls DICOM-bilde DICOM-afbeelding DICOM-bilete imatge DICOM Obraz DICOM imagem DICOM Imagem DICOM Imagine DICOM изображение DICOM Obrázok DICOM Slikovna datoteka DICOM Figurë DICOM ДИКОМ слика DICOM-bild DICOM görüntüsü зображення DICOM Ảnh DICOM DICOM 图像 DICOM 影像 DICOM Digital Imaging and Communications in Medicine DocBook document مستند DocBook Dakument DocBook Документ — DocBook document DocBook dokument DocBook DocBook-dokument DocBook-Dokument Έγγραφο DocBook DocBook document DocBook-dokumento documento DocBook DocBook dokumentua DocBook-asiakirja DocBook skjal document DocBook cáipéis DocBook documento de DocBook מסמך DocBook DocBook dokument DocBook dokumentum Documento DocBook Dokumen DocBook Documento DocBook DocBook ドキュメント DocBook-ის დოკუმენტი DocBook құжаты DocBook 문서 DocBook dokumentas DocBook dokuments DocBook-dokument DocBook-document DocBook-dokument document DocBook Dokument DocBook documento DocBook Documento DocBook Document DocBook документ DocBook Dokument DocBook Dokument DocBook Dokument DocBook Док Бук документ DocBook-dokument DocBook belgesi документ DocBook Tài liệu DocBook DocBook 文档 DocBook 文件 DIB image صورة DIB Vyjava DIB Изображение — DIB imatge DIB obrázek DIB DIB-billede DIB-Bild Εικόνα DIB DIB image DIB-bildo imagen DIB DIB irudia DIB-kuva DIB mynd image DIB íomhá DIB imaxe DIB תמונת DIB DIB slika DIB kép Imagine DIB Citra DIB Immagine DIB DIB 画像 DIB გამოსახულება DIB суреті DIB 그림 DIB paveikslėlis DIB attēls DIB-bilde DIB-afbeelding DIB-bilete imatge DIB Obraz DIB imagem DIB Imagem DIB Imagine DIB изображение DIB Obrázok DIB Slikovna datoteka DIB Figurë DIB ДИБ слика DIB-bild DIB görüntüsü зображення DIB Ảnh DIB DIB 图像 DIB 影像 DIB Device Independent Bitmap DjVu image صورة DjVu Vyjava DjVu Изображение — DjVu imatge DjVu obrázek DjVu DjVu-billede DjVu-Bild Εικόνα DjVu DjVu image DjVu-bildo imagen DjVu DjVU-ko irudia DjVu-kuva DjVu mynd image DjVu íomhá DjVu imaxe de DjVu תמונת DjVu DjVu slika DjVu-kép Imagine DjVu Citra DjVu Immagine DjVu DjVu 画像 DjVu გამოსახულება DjVu суреті DjVu 그림 DjVu paveikslėlis DjVu attēls Imej DjVu DjVu-bilde DjVu-afbeelding DjVu-bilete imatge DjVu Obraz DjVu imagem DjVu Imagem DjVu Imagine DjVu изображение DjVu Obrázok DjVu Slikovna datoteka DjVu Figurë DjVu ДјВу слика DjVu-bild DjVu görüntüsü зображення DjVu Ảnh DjVu DjVu 图像 DjVu 影像 DjVu document document DjVu dokument DjVu DjVu-dokument DjVu-Dokument Έγγραφο DjVu DjVu document documento DjVu DjVu dokumentua DjVu-asiakirja DjVu dokument DjVu dokumentum Documento DjVu Dokumen DjVu Documento DjVu DjVu құжаты DjVu 문서 document DjVu Dokument DjVu documento DjVu Documento DjVu документ DjVu Dokument DjVu ДјВу документ DjVu-dokument DjVu belgesi документ DjVu DjVu 文档 DjVu 文件 DPX image صورة DPX Vyjava DPX Изображение — DPX imatge DPX obrázek DPX DPX-billede DPX-Bild Εικόνα DPX DPX image DPX-bildo imagen DPX DPX irudia DPX-kuva DPX mynd image DPX íomhá DPX imaxe DPX תמונת DPX DPX slika DPX kép Imagine DPX Citra DPX Immagine DPX DPX 画像 DPX გამოსახულება DPX суреті DPX 그림 DPX paveikslėlis DPX attēls DPX-bilde DPX-afbeelding DPX-bilete imatge DPX Obraz DPX imagem DPX Imagem DPX Imagine DPX изображение DPX Obrázok DPX Slikovna datoteka DPX Figurë DPX ДПИкс слика DPX-bild DPX görüntüsü зображення DPX Ảnh DPX DPX 图像 DPX 影像 DPX Digital Moving Picture Exchange EPS image صورة EPS Vyjava EPS Изображение — EPS imatge EPS obrázek EPS EPS-billede EPS-Bild Εικόνα EPS EPS image EPS-bildo imagen EPS EPS irudia EPS-kuva EPS mynd image EPS íomhá EPS imaxe EPS תמונת EPS EPS slika EPS kép Imagine EPS Citra EPS Immagine EPS EPS 画像 EPS გამოსახულება EPS суреті EPS 그림 EPS paveikslėlis EPS attēls EPS-bilde EPS-afbeelding EPS-bilete imatge EPS Obraz EPS imagem EPS Imagem EPS Imagine EPS изображение EPS Obrázok EPS Slikovna datoteka EPS Figurë EPS ЕПС слика EPS-bild EPS görüntüsü зображення EPS Ảnh EPS EPS 图像 EPS 影像 EPS Encapsulated PostScript FITS document مستند FITS Dakument FITS Документ — FITS document FITS dokument FITS FITS-dokument FITS-Dokument Έγγραφο FITS FITS document FITS-dokumento documento FITS FITS dokumentua FITS-asiakirja FITS skjal document FITS cáipéis FITS documento FICT מסמך FITS FITS dokument FITS dokumentum Documento FITS Dokumen FITS Documento FITS FITS ドキュメント FITS დოკუმენტი FITS құжаты FITS 문서 FITS dokumentas FITS dokuments FITS-dokument FITS-document FITS-dokument document FITS Dokument FITS documento FITS Documento FITS Document FITS документ FITS Dokument FITS Dokument FITS Dokument FITS ФИТС документ FITS-dokument FITS belgesi документ FITS Tài liệu FITS FITS 文档 FITS 文件 FITS Flexible Image Transport System FPX image صورة FPX Vyjava FPX Изображение — FPX imatge FPX obrázek FPX FPX-billede FPX-Bild Εικόνα FPX FPX image FPX-bildo imagen FPX FPX irudia FPX-kuva FPX mynd image FPX íomhá FPX imaxe FPX תמונת FPX FPX slika FPX kép Imagine FPX Citra FPX Immagine FPX FPX 画像 FPX გამოსახულება FPX суреті FPX 그림 FPX paveikslėlis FPX attēls FPX-bilde FPX-afbeelding FPX-bilete imatge FPX Obraz FPX imagem FPX Imagem FPX Imagine FPX изображение FPX Obrázok FPX Slikovna datoteka FPX Figurë FPX ФПИкс слика FPX-bild FPX görüntüsü зображення FPX Ảnh FPX FPX 图像 FPX 影像 FPX FlashPiX EPS image (gzip-compressed) صورة EPS (مضغوط-gzip) Vyjava EPS (gzip-skampresavanaja) Изображение — EPS, компресирано с gzip imatge EPS (amb compressió gzip) obrázek EPS (komprimovaný pomocí gzip) EPS-billede (gzip-komprimeret) EPS-Bild (gzip-komprimiert) Εικόνα EPS (συμπιεσμένη gzip) EPS image (gzip-compressed) imagen EPS (comprimida con gzip) EPS irudia (gzip-ekin konprimitua) EPS-kuva (gzip-pakattu) EPS mynd (gzip-stappað) image EPS (compressée gzip) íomhá EPS (comhbhrúite le gzip) imaxe EPS (comprimida con gzip) תמונת EPS (מכווץ ע״י gzip) EPS slika (komprimirana gzip-om) EPS kép (gzip-tömörítésű) Imagine EPS (comprimite con gzip) Citra EPS (terkompresi gzip) Immagine EPS (compressa con gzip) EPS 画像 (gzip 圧縮) EPS გამოსახულება (gzip-ით შეკუმშული) EPS суреті (gzip-пен сығылған) EPS 그림(GZIP 압축) EPS paveikslėlis (suglaudintas su gzip) EPS attēls (saspiests ar gzip) EPS-bilde (gzip-komprimert) EPS-afbeelding (ingepakt met gzip) EPS-bilete (pakka med gzip) imatge EPS (compressat gzip) Obraz EPS (kompresja gzip) imagem EPS (compressão gzip) Imagem EPS (compactada com gzip) Imagine EPS (compresie gzip) изображение EPS (сжатое gzip) Obrázok EPS (komprimovaný pomocou gzip) Slikovna datoteka EPS (stisnjena z gzip) Figurë EPS (e kompresuar me gzip) ЕПС слика (запакована гзип-ом) EPS-bild (gzip-komprimerad) EPS görüntüsü (gzip ile sıkıştırılmış) зображення EPS (стиснене gzip) Ảnh EPS (đã nén gzip) EPS 图像(gzip 压缩) EPS 影像 (gzip 格式壓縮) Windows icon icona de Windows ikona Windows Windows-ikon Windows-Symbol Εικονίδιο Windows Windows icon icono de Windows Windows ikonoa Windows-kuvake Windows ikona Windows ikon Icone pro Windows Ikon Windows Icona Windows Windows таңбашасы 윈도우 아이콘 icòna Windows Ikona Windows ícone Windows Ícone do Windows значок Windows Ikona Windows Ikona Windows Иконица Виндоуза Windows-ikon Windows simgesi піктограма Windows Windows 图标 Windows 圖示 MacOS X icon أيقونة MacOS X Ikona MacOS X Икона — MacOS X icona MacOS X ikona MacOS X MacOS X-ikon MacOS-X-Symbol Εικονίδιο MacOS X MacOS X icon MacOS-X-piktogramo icono de OS X MacOS X ikonoa MacOS X -kuvake MacOS X ímynd icône MacOS X deilbhín MacOS X Icona de MacOS X סמל בתקן MacOS X MacOS X ikona MacOS X ikon Icone de Mac OS X Ikon MacOS X Icona MacOS X MacOS X アイコン MacOS X-ის ხატულა MacOS X таңбашасы Mac OS X 아이콘 MacOS X piktograma MacOS X ikona MacOS X-ikon MacOS-X-pictogram MacOS X-ikon icòna MacOS X Ikona Mac OS X ćone MacOS X Ícone do MacOS X Iconiță MacOS X значок MacOS X Ikona MacOS X Datoteka ikone MacOS X Ikonë MacOS X МекОС Икс иконица MacOS X-ikon MacOS X simgesi піктограма MacOS X Biểu tượng MacOS X MacOS X 图标 MacOS X 圖示 ILBM image صورة ILBM ILBM rəsmi Vyjava ILBM Изображение — ILBM imatge ILBM obrázek ILMB Delwedd ILBM ILBM-billede ILBM-Bild Εικόνα ILBM ILBM image ILBM-bildo imagen ILBM ILBM irudia ILBM-kuva ILBM mynd image ILBM íomhá ILBM imaxe ILBM תמונת ILBM ILBM slika ILBM-kép Imagine ILBM Citra ILBM Immagine ILBM ILBM 画像 ILBM суреті ILBM 그림 ILBM paveikslėlis ILBM attēls Imej ILBM ILBM-bilde ILBM-afbeelding ILMB-bilete imatge ILBM Obraz ILBM imagem ILBM Imagem ILBM Imagine ILBM изображение ILBM Obrázok ILMB Slikovna datoteka ILBM Figurë ILBM ИЛБМ слика ILBM-bild ILBM görüntüsü зображення ILBM Ảnh ILBM ILBM 图像 ILBM 影像 ILBM InterLeaved BitMap JNG image صورة JNG JNG rəsmi Vyjava JNG Изображение — JNG imatge JNG obrázek JNG Delwedd JNG JNG-billede JNG-Bild Εικόνα JNG JNG image JNG-bildo imagen JNG JNG irudia JNG-kuva JNG mynd image JNG íomhá JNG imaxe JNG תמונת JNG JNG slika JNG-kép Imagine JNG Citra JNG Immagine JNG JNG 画像 JNG суреті JNG 그림 JNG paveikslėlis JNG attēls Imej PNG JNG-bilde JNG-afbeelding JNG-bilete imatge JNG Obraz JNG imagem JNG Imagem JNG Imagine JNG изображение JNG Obrázok JNG Slikovna datoteka JNG Figurë JNG ЈНГ слика JNG-bild JNG görüntüsü зображення JNG Ảnh JNG JNG 图像 JNG 影像 JNG JPEG Network Graphics LightWave object كائن LightWave LightWave cismi Abjekt LightWave Обект — LightWave objecte de LightWave objekt LightWave Gwrthrych LightWave LightWave-objekt LightWave-Objekt Αντικείμενο LightWave LightWave object LightWave-objekto objeto de LightWave LightWave objektua LightWave-esine LightWave lutur objet LightWave réad LightWave obxecto de LightWave עצם LightWave LightWave objekt LightWave-objektum Objecto LightWave Proyek LightWave Oggetto LightWave LightWave オブジェクト LightWave объекті LightWave 개체 LightWave objektas LightWave objekts Objek LightWave LightWave-objekt LightWave-object LightWave-objekt objècte LightWave Obiekt LightWave Objecto LightWave Objeto LightWave Obiect LightWave объект LightWave Objekt LightWave Datoteka predmeta LightWave Objekt LightWave Лајт Вејв објекат LightWave-objekt LightWave nesnesi об'єкт LightWave Đối tượng LightWave LightWave 对象 LightWave 物件 LightWave scene مشهد LightWave LightWave səhnəsi Scena LightWave Сцена — LightWave escena de LightWave scéna LightWave Golygfa LightWave LightWave-scene LightWave-Szene Σκηνή LightWave LightWave scene LightWave-sceno escena de LightWave LightWave eszena LightWave-maisema LightWave leikmynd scène LightWave radharc LightWave escena de LightWave סצנה של LightWave LightWave scena LightWave-jelenet Scena LightWave Scene LightWave Scena LightWave LightWave シーン LightWave сахнасы LightWave 장면 LightWave scena LightWave aina Babak LightWave LightWave-scene LightWave-scène LightWave-scene scèna LightWave Scena Lightwave cenário LightWave Cena LightWave Scenă LightWave сцена LightWave Scéna LightWave Datoteka scene LightWave Skenë LightWave Лајт Вејв сцена LightWave-scen LightWave sahnesi сцена LightWave Cảnh LightWave LightWave 场景 LightWave 場景 MacPaint Bitmap image صورة MacPaint Bitmap Bitmapnaja vyjava MacPaint Изображение — MacPaint Bitmap imatge de mapa de bits MacPaint obrázek MacPaint Bitmap MacPaint BitMap-billede MacPaint-Bitmap-Datei Εικόνα Bitmap MacPaint MacPaint Bitmap image imagen de mapa de bits de MacPaint MacPaint Bitmap irudia MacPaint-bittikartta MacPaint Bitmap mynd image matricielle MacPaint íomhá MacPaint Bitmap imaxe de mapa de bits MacPaint תמונת מפת-סיביות של MacPaint MacPaint Bitmap slika MacPaint bitkép Imagine bitmap de MacPaint Citra MacPaint Bitmap Immagine Bitmap MacPaint MacPaint ビットマップ画像 MacPaint растрлық суреті MacPaint 비트맵 그림 MacPaint rastrinis paveikslėlis MacPaint bitkartes attēls MacPaint Bitmap-bilde MacPaint-bitmap-afbeelding MacPaint punktbilete imatge matricial MacPaint Obraz bitmapowy MacPaint imagem MacPaint Bitmap Imagem de bitmap do MacPaint Imagine MacPaint Bitmap растровое изображение MacPaint Obrázok MacPaint Bitmap Slikovna bitna datoteka MacPaint Figurë BitMap MacPaint Мек Пеинт битмап слика MacPaint Bitmap-bild MacPaint bit eşlem görüntüsü растрове зображення MacPaint Ảnh mảng MacPaint MacPaint 位图 MacPaint 點陣影像 Office drawing تصميم أوفيس Ofisny rysunak Чертеж — Office dibuix d'Office kresba Office Officetegning Office-Zeichnung Σχέδιο Office Office drawing dibujo de Office Office marrazkia Office-piirros Office tekning dessin Office líníocht Office debuxo de Office ציור של Office Office crtež Office rajz Designo Office Gambar Office Disegno Office Office ドロー Office суреті 오피스 그리기 Office piešinys Office zīmējums Office-tegning Office-tekening Office-teikning dessenh Office Rysunek Office desenho Office Desenho do Office Desen Office изображение Office Kresba Office Datoteka risbe Office Vizatim Office Канцеларијски цртеж Office-teckning Ofis çizimi малюнок Office Bản vẽ Office Microsoft Office 绘图 Office 繪圖 NIFF image صورة NIFF Vyjava NIFF Изображение — NIFF imatge NIFF obrázek NIFF NIFF-billede NIFF-Bild Εικόνα NIFF NIFF image NIFF-bildo imagen NIFF NIFF irudia NIFF-kuva NIFF mynd image NIFF íomhá NIFF imaxe NIFF תמונת NIFF NIFF slika NIFF kép Imagine NIFF Citra NIFF Immagine NIFF NIFF 画像 NIFF суреті NIFF 그림 NIFF paveikslėlis NIFF attēls NIFF-bilde NIFF-afbeelding NIFF-bilete imatge NIFF Obraz NIFF imagem NIFF Imagem NIFF Imagine NIF изображение NIFF Obrázok NIFF Slikovna datoteka NIFF Figurë NIFF НИФФ слика NIFF-bild NIFF görüntüsü зображення NIFF Ảnh NIFF NIFF 图像 NIFF 影像 PCX image صورة PCX Vyjava PCX Изображение — PCX imatge PCX obrázek PCX PCX-billede PCX-Bild Εικόνα PCX PCX image PCX-bildo imagen PCX PCX irudia PCX-kuva PCX mynd image PCX íomhá PCX imaxe PCX תמונת PCX PCX slika PCX kép Imagine PCX Citra PCX Immagine PCX PCX 画像 PCX суреті PCX 그림 PCX paveikslėlis PCX attēls PCX-bilde PCX-afbeelding PCX-bilete imatge PCX Obraz PCX imagem PCX Imagem PCX Imagine PCX изображение PCX Obrázok PCX Slikovna datoteka PCX Figurë PCX ПЦИкс слика PCX-bild PCX görüntüsü зображення PCX Ảnh PCX PCX 图像 PCX 影像 PCX PiCture eXchange PCD image صورة PCD Vyjava PCD Изображение — PCD imatge PCD obrázek PCD PCD-billede PCD-Bild Εικόνα PCD PCD image PCD-bildo imagen PCD PCD irudia PCD-kuva PCD mynd image PCD íomhá PCD imaxe PCD תמונת PCD PCD slika PCD kép Imagine PCD Citra PCD Immagine PCD PCD 画像 PCD გამოსახულება PCD суреті PCD 그림 PCD paveikslėlis PCD attēls PCD-bilde PCD-afbeelding PCD-bilete imatge PCD Obraz PCD imagem PCD Imagem PCD Imagine PCD изображение PCD Obrázok PCD Slikovna datoteka PCD Figurë PCD ПЦД слика PCD-bild PCD görüntüsü зображення PCD Ảnh PCD PCD 图像 PCD 影像 PCD PhotoCD PNM image صورة PNM PNM rəsmi Vyjava PNM Изображение — PNM imatge PNM obrázek PNM Delwedd PNM PNM-billede PNM-Bild Εικόνα PNM PNM image PNM-bildo imagen PNM PNM irudia PNM-kuva PNM mynd image PNM íomhá PNM imaxe PNM תמונת PNM PNM slika PNM-kép Imagine PNM Citra PNM Immagine PNM PNM 画像 PNM суреті PNM 그림 PNM paveikslėlis PNM attēls Imej PNM PNM-bilde PNM-afbeelding PNM-bilete imatge PNM Obraz PNM imagem PNM Imagem PNM Imagine PNM изображение PNM Obrázok PNM Slikovna datoteka PNM Figurë PNM ПНМ слика PNM-bild PNM görüntüsü зображення PNM Ảnh PNM PNM 图像 PNM 影像 PBM image صورة PBM Vyjava PBM Изображение — PBM imatge PBM obrázek PBM Delwedd PBM PBM-billede PBM-Bild Εικόνα PBM PBM image PBM-bildo imagen PBM PBM irudia PBM-kuva PBM mynd image PBM íomhá PBM imaxe PBM תמונת PBM PBM slika PBM kép Imagine PBM Citra PBM Immagine PBM PBM 画像 PBM გამოსახულება PBM суреті PBM 그림 PBM paveikslėlis PBM attēls PBM-bilde PBM-afbeelding PBM-bilete imatge PBM Obraz PBM imagem PBM Imagem PBM Imagine PBM изображение PBM Obrázok PBM Slikovna datoteka PBM Figurë PBM ПБМ слика PBM-bild PBM görüntüsü зображення PBM Ảnh PBM PBM 图像 PBM 影像 PBM Portable BitMap PGM image صورة PGM Vyjava PGM Изображение — PGM imatge PGM obrázek PGM Delwedd PGM PGM-billede PGM-Bild Εικόνα PGM PGM image PGM-bildo imagen PGM PGM irudia PGM-kuva PGM mynd image PGM íomhá PGM imaxe PGM תמונת PGM PGM slika PGM kép Imagine PGM Citra PGM Immagine PGM PGM 画像 PGM суреті PGM 그림 PGM paveikslėlis PGM attēls PGM-bilde PGM-afbeelding PGM-bilete imatge PGM Obraz PGM imagem PGM Imagem PGM Imagine PGM изображение PGM Obrázok PGM Slikovna datoteka PGM Figurë PGM ПГМ слика PGM-bild PGM görüntüsü зображення PGM Ảnh PGM PGM 图像 PGM 影像 PGM Portable GrayMap PPM image صورة PPM Vyjava PPM Изображение — PPM imatge PPM obrázek PPM Delwedd PPM PPM-billede PPM-Bild Εικόνα PPM PPM image PPM-bildo imagen PPM PPM irudia PPM-kuva PPM mynd image PPM íomhá PPM imaxe PPM תמונת PPM PPM slika PPM kép Imagine PPM Citra PPM Immagine PPM PPM 画像 PPM суреті PPM 그림 PPM paveikslėlis PPM attēls PPM-bilde PPM-afbeelding PPM-bilete imatge PPM Obraz PPM imagem PPM Imagem PPM Imagine PPM изображение PPM Obrázok PPM Slikovna datoteka PPM Figurë PPM ППМ слика PPM-bild PPM görüntüsü зображення PPM Ảnh PPM PPM 图像 PPM 影像 PPM Portable PixMap Photoshop image صورة فوتوشوب Изображение — Photoshop imatge de Photoshop obrázek Photoshop Photoshop-billede Photoshop-Bild Εικόνα Photoshop Photoshop image Photoshop-bildo imagen de Photoshop Photoshop irudia Photoshop-kuva Photoshop mynd image Photoshop íomhá Photoshop imaxe de Photoshop תמונת Photoshop Photoshop slika Photoshop-kép Imagine Photoshop Citra Photoshop Immagine Photoshop Photoshop 画像 изображение Photoshop 포토샵 이미지 Photoshop paveikslėlis Photoshop attēls Imej Photoshop Photoshop-afbeelding imatge Photoshop Obraz Photoshop imagem Photoshop Imagem do Photoshop Imagine Photoshop изображение Photoshop Obrázok Photoshop Slikovna datoteka Photoshop Фотошоп слика Photoshop-bild Photoshop görüntüsü зображення Photoshop Ảnh Photoshop Photoshop 图像 Photoshop 影像 RGB image صورة RGB RGB rəsmi Vyjava RGB Изображение — RGB imatge RGB obrázek RGB Delwedd RGB RGB-billede RGB-Bild Εικόνα RGB RGB image RGB-bildo imagen RGB RGB irudia RGB-kuva RGB mynd image RGB íomhá RGB imaxe RGB תמונת RGB RGB slika RGB-kép Imagine RGB Citra RGB Immagine RGB RGB 画像 RGB суреті RGB 그림 RGB paveikslėlis RGB attēls Imej RGB RGB-bilde RGB-afbeelding RGB-bilete imatge RGB Obraz RGB imagem RGB Imagem RGB Imagine RGB изображение RGB Obrázok RGB Slikovna datoteka RGB Figurë RGB РГБ слика RGB-bild RGB görüntüsü зображення RGB Ảnh kiểu RGB RGB 图像 RGB 影像 SGI image صورة SGI Vyjava SGI Изображение — SGI imatge SGI obrázek SGI SGI-billede SGI-Bild Εικόνα SGI SGI image SGI-bildo imagen SGI SGI irudia SGI-kuva SGI mynd image SGI íomhá SGI imaxe SGI תמונת SGI SGI slika SGI kép Imagine SGI Citra SGI Immagine SGI SGI 画像 SGI суреті SGI 그림 SGI paveikslėlis SGI attēls SGI-bilde SGI-afbeelding SGI-bilete imatge SGI Obraz SGI imagem SGI Imagem SGI Imagine SGI изображение SGI Obrázok SGI Slikovna datoteka SGI Figurë SGI СГИ слика SGI-bild SGI görüntüsü зображення SGI Ảnh SGI SGI 图像 SGI 影像 Sun raster image صورة Sun raster Rastravaja vyjava Sun Изображение — Sun raster imatge ràster Sun rastrový obrázek Sun Sun rasterbillede Sun-Rasterbild Εικόνα Sun raster Sun raster image imagen rasterizada de Sun Sun raster irudia Sun-rasterikuva Sun raster mynd image raster Sun íomhá rastar Sun imaxe ráster de Sun תמונה סרוקה של Sun Sun iscrtana slika SUN raszterkép Imagine raster Sun Citra raster Sun Immagine raster Sun Sun ラスタ画像 Sun растрлық суреті Sun 래스터 그림 Sun rastrinis paveikslėlis Sun rastra attēls Sun rasterbilde Sun-rasterafbeelding Sun rasterbilete imatge raster Sun Obraz rastrowy Sun imagem raster Sun Imagem raster da Sun Imagine rasterizată Sun растровое изображение Sun Rastrový obrázok Sun Slikovna rastrska datoteka Sun Figurë raster Sun слика Сановог растера Sun-rasterbild Sun raster görüntüsü растрове зображення Sun Ảnh mành Sun Sun 光栅图像 Sun raster 影像 TGA image صورة TGA Vyjava TGA Изображение — TGA imatge TGA obrázek TGA TGA-billede TGA-Bild Εικόνα TGA TGA image TGA-bildo imagen TGA TGA irudia TGA-kuva TGA mynd image TGA íomhá TGA imaxe TGA תמונת TGA TGA slika TGA kép Imagine TGA Citra TGA Immagine TGA TGA 画像 TGA суреті TGA 그림 TGA paveikslėlis TGA attēls TGA-bilde TGA-afbeelding TGA-bilete imatge TGA Obraz TGA imagem TGA Imagem TGA Imagine TGA изображение TGA Obrázok TGA Slikovna datoteka TGA Figurë TGA ТГА слика TGA-bild TGA görüntüsü зображення TGA Ảnh TGA TGA 图像 TGA 影像 TGA Truevision Graphics Adapter Windows cursor مؤشر ويندوز Kursor Windows Курсор — Windows cursor de Windows kurzor Windows Windowsmarkør Windows-Cursor Δρομέας Windows Windows cursor Windows-kursoro cursor de Windows Windows kurtsorea Windows-osoitin Windows vísi curseur Windows cúrsóir Windows Cursor de Windows סמן של Windows Windows kursor Windows-kurzor Cursor pro Windows Kursor Windows Cursore Windows Windows カーソル Windows курсоры Windows 커서 Windows žymiklis Windows kursors Kursor Windows Windows-markør Windows-muisaanwijzer Windows-peikar cursor Windows Kursor Windows cursor Windows Cursor do Windows Cursor Windows курсор Windows Kurzor Windows Datoteka kazalke Windows Kursor Windows Виндоузов курсор Windows-muspekare Windows imleci курсор Windows Con chạy Windows Windows 光标 Windows 滑鼠游標 Windows animated cursor مؤشر ويندوز المتحرك Animavany kursor Windows Курсор — Windows, анимиран cursor animat de Windows animovaný kurzor Windows Windowsanimeret markør Animierter Windows-Cursor Κινούμενος δρομέας Windows Windows animated cursor cursor animado de Windows Windows-eko kurtsore animatua animoitu Windows-osoitin Windows livindaigjørdur vísi curseur animé Windows cúrsóir beo Windows Cursor animado de Windows סמן מונפש של Windows Windows animirani kursor Windows animált kurzor Cursor animate pro Windows Kursor animasi Windows Cursore animato Windows Windows アニメーションカーソル Windows анимациясы бар курсор Windows 움직이는 커서 Animuotas Windows žymiklis Windows animēts kursors geanimeerde Windows-muisaanwijzer Windows animert peikar cursor animat Windows Animowany kursor Windows cursor animado Windows Cursor animado do Windows Cursor animat Windows анимированный курсор Windows Animovaný kurzor Windows Datoteka animirane kazalke Windows Kursor i animuar Windows Виндоузов анимирани курсор Animerad Windows-muspekare Windows canlandırmalı imleci анімований курсор Windows Con chạy hoạt họa Windows Windows 动画光标 Windows 滑鼠動畫游標 EMF image صورة EMF Vyjava EMF Изображение — EMF imatge EMF obrázek EMF EMF-billede EMF-Bild Εικόνα EMF EMF image EMF-bildo imagen EMF EMF irudia EMF-kuva EMF mynd image EMF íomhá EMF imaxe EMF תמונת EMF EMF slika EMF kép Imagine EMF Citra EMF Immagine EMF EMF 画像 EMF გამოსახულება EMF суреті EMF 그림 EMF paveikslėlis EMF attēls EMF-bilde EMF-afbeelding EMF-bilete imatge EMF Obraz EMF imagem EMF Imagem EMF Imagine EMF изображение EMF Obrázok EMF Slikovna datoteka EMF Figurë EMF ЕМФ слика EMF-bild EMF görüntüsü зображення EMF Ảnh EMF EMF 图像 EMF 影像 EMF Enhanced MetaFile WMF image صورة WMF Vyjava WMF Изображение — WMF imatge WMF obrázek WMF WMF-billede WMF-Bild Εικόνα WML WMF image WMF-bildo imagen WMF WMF irudia WMF-kuva WMF mynd image WMF íomhá WMF imaxe WMF תמונת WMF WMF slika WMF kép Imagine WMF Citra WMF Immagine WMF WMF 画像 WMF суреті WMF 그림 WMF paveikslėlis WMF attēls WMF-bilde WMF-afbeelding WMF-bilete imatge WMF Obraz WMF imagem WMF Imagem WMF Imagine WMF изображение WMF Obrázok WMF Slikovna datoteka WMF Figurë WMF ВМФ слика WMF-bild WMF görüntüsü зображення WMF Ảnh WMF WMF 图像 WMF 影像 WMF Windows Metafile XBM image صورة XBM Vyjava XBM Изображение — XBM imatge XBM obrázek XBM XBM-billede XBM-Bild Εικόνα XBM XBM image XBM-bildo imagen XBM XBM irudia XBM-kuva XBM mynd image XBM íomhá XBM imaxe XBM תמונת XBM XBM slika XBM-kép Imagine XBM Citra XBM Immagine XBM XBM 画像 XBM суреті XBM 그림 XBM paveikslėlis XBM attēls XBM-bilde XBM-afbeelding XBM-bilete imatge XBM Obraz XBM imagem XBM Imagem XBM Imagine XBM изображение XBM Obrázok XBM Slikovna datoteka XBM Figurë XBM ИксБМ слика XBM-bild XBM görüntüsü зображення XBM Ảnh XBM XBM 图像 XBM 影像 XBM X BitMap GIMP image صورة GIMP Vyjava GIMP Изображение — GIMP imatge de GIMP obrázek GIMP GIMP-billede GIMP-Bild Εικόνα GIMP GIMP image GIMP-bildo imagen del GIMP GIMP irudia GIMP-kuva GIMP mynd image GIMP íomhá GIMP imaxe de GIMP תמונת GIMP GIMP slika GIMP-kép Imagine GIMP Citra GIMP Immagine GIMP GIMP 画像 GIMP გამოსახულება GIMP суреті GIMP 그림 GIMP paveikslėlis GIMP attēls Imej GIMP GIMP-bilde GIMP-afbeelding GIMP-bilete imatge GIMP Obraz GIMP imagem GIMP Imagem do GIMP Imagine GIMP изображение GIMP Obrázok GIMP Slikovna datoteka GIMP Figurë GIMP Гимпова слика GIMP-bild GIMP görüntüsü зображення GIMP Ảnh GIMP GIMP 图像 GIMP 影像 XFig image صورة XFig Vyjava XFig Изображение — XFig imatge de XFig obrázek XFig XFig-billede XFig-Bild Εικόνα XFig XFig image XFig-bildo imagen de XFig XFig irudia XFig-kuva XFig mynd image XFig íomhá XFig imaxe de XFig תמונת XFig XFig slika XFig-kép Imagine XFig Citra XFig Immagine XFig XFig 画像 XFig суреті XFig 그림 XFig paveikslėlis XFig attēls Imej XFig XFig-bilde XFig-afbeelding XFig-bilete imatge XFig Obraz XFig imagem XFig Imagem do XFig Imagine XFig изображение XFig Obrázok XFig Slikovna datoteka XFig Figurë XFig ИксФиг слика XFig-bild XFig görüntüsü зображення XFig Ảnh XFig XFig 图像 XFig 影像 XPM image صورة XPM Vyjava XPM Изображение — XPM imatge XPM obrázek XPM Delwedd XPM XPM-billede XPM-Bild Εικόνα XPM XPM image XPM-bildo imagen XPM XPM irudia XPM-kuva XPM mynd image XPM íomhá XPM imaxe XPM תמונת XPM XPM slika XPM kép Imagine XPM Citra XPM Immagine XPM XPM 画像 XPM суреті XPM 그림 XPM paveikslėlis XPM attēls XPM-bilde XPM-afbeelding XPM-bilete imatge XPM Obraz XPM imagem XPM Imagem XPM Imagine XPM изображение XPM Obrázok XPM Slikovna datoteka XPM Figurë XPM ИксПМ слика XPM-bild XPM görüntüsü зображення XPM Ảnh XPM XPM 图像 XPM 影像 XPM X PixMap X window image صورة X window X window rəsmi Vyjava vakna X Изображение — X Window imatge de X window obrázek X window Delwedd ffenest X X-billede X-Window-Bild Εικόνα περιβάλλοντος X X window image bildo de X window imagen de ventana de X X window irudia X-ikkunakuva X vindeyga mynd image X window íomhá fhuinneog X imaxe de X Window תמונת חלון של X X window slika X window-kép Imagine X Window Citra X window Immagine X window X window 画像 X window суреті X 윈도 그림 X window paveikslėlis X window attēls Imej tetingkap X X-Windows skjermbilde X-window-afbeelding X window bilete imatge X window Obraz X Window imagem de janela X Imagem de janela do X Imagine X window изображение X window Obrázok X window slika X oken Figurë X window слика Икс прозора X-fönsterbild X pencere görüntüsü зображення X window Ảnh cửa sổ X X window 图像 X window 影像 block device جهاز كتلي blokavaja pryłada Блоково устройство dispositiu de blocs blokové zařízení blokenhed Blockorientiertes Gerät Συσκευή block block device bloka disponaĵo dispositivo de bloques bloke-gailua laitetiedosto blokka tóleind périphérique de blocs gléas bloc dispositivo de bloque התקן בלוק Blokovski uređaj blokkos eszköz Dispositivo de blocos blok divais Device a blocchi ブロックデバイス блоктық құрылғысы 블록 장치 blokinis įrenginys bloka ierīce Peranti blok blokkenhet blok-apparaat blokk-eining periferic de blòts Urządzenie blokowe dispositivo de bloco Dispositivo de bloco dispozitiv bloc блочное устройство Blokové zariadenie bločna naprava device me blloqe блок уређај blockenhet blok aygıtı блоковий пристрій thiết bị khối 块设备 區塊裝置 character device جهاز حرفي znakavaja pryłada Символно устройство dispositiu de caràcters znakové zařízení tegnenhed Zeichenorientiertes Gerät Συσκευή χαρακτήρων character device signa disponaĵo dispositivo de caracteres karaktereen gailua merkkilaite stavatóleind périphérique de caractères gléas carachtar dispositivo de caracter התקן תכונה Znakovni uređaj karakteres eszköz Dispositivo de characteres karakter divais Device a caratteri キャラクタデバイス символдық құрылғысы 문자 장치 simbolinis įrenginys rakstzīmju ierīce Peranti aksara tegnenhet byte-apparaat teikneining periferic de caractèrs Urządzenie znakowe dispositivo de caracteres Dispositivo de caractere dispozitiv caracter символьное устройство Znakové zariadenie znakovna naprava device me karaktere знаковни уређај teckenenhet karakter aygıtı символьний пристрій thiết bị ký tự 字符设备 字元裝置 folder مجلّد kataloh Папка carpeta složka mappe Ordner Φάκελος folder dosierujo carpeta karpeta kansio mappa dossier fillteán cartafol תיקייה direktorij mappa Dossier folder Cartella フォルダー бума 폴더 aplankas mape Folder mappe map mappe dorsièr Katalog pasta Pasta dosar папка Priečinok mapa Kartelë фасцикла mapp dizin тека thư mục 文件夹 資料夾 pipe إنبوب kanvejer Конвейер conducte roura datakanal Pipe Διοχέτευση pipe dukto tubería kanalizazioa putki rør tube píopa tubería צינור Slivnik adatcsatorna Tubo pipa Pipe パイプ арна 파이프 konvejeris programmkanāls Paip rør pijp røyr tub Potok canal Pipe canal pipe канал Rúra cev Pipe спојка rör boru канал ống dẫn 管道 管線 mount point نقطة الوصْل punkt mantavańnia Точка на монтиране punt de muntatge přípojné místo monteringspunkt Einhängepunkt Σημείο προσάρτησης mount point surmetingo punto de montaje muntatze-puntua liitospiste ísetingarpunkt point d'accès pointe feistithe punto de montaxe נקודת עיגון točka montiranja csatolási pont Puncto de montage titik mount Punto di mount マウントポイント тіркеу нүктесі 마운트 위치 prijungimo taškas montēšanas punkts Titik lekapan monteringspunkt aankoppelingspunt monteringspunkt punt d'accès Punkt montowania ponto de montagem Ponto de montagem loc montare точка монтирования Miesto pripojenia priklopna točka Pikë montimi тачка прикључења monteringspunkt bağlama noktası точка монтування điểm lắp 挂载点 掛載點 socket مقبس sokiet Гнездо sòcol socket sokkel Socket Υποδοχή socket kontaktoskatolo socket socketa pistoke sokkul connecteur réseau soicéad socket נקודת חיבור utičnica illesztőpont Socket soket Socket ソケット сокет 소켓 lizdas sokets Soket plugg socket sokkel connector ret Gniazdo tomada Socket socket сокет Soket vtič Socket прикључница uttag soket сокет ổ cắm 套接字 socket symbolic link وصلة رمزية simvolik körpü symbalnaja spasyłka Символна връзка enllaç simbòlic symbolický odkaz cyswllt symbolaidd symbolsk henvisning Symbolische Verknüpfung Συμβολικός σύνδεσμος symbolic link simbola ligilo enlace simbólico esteka sinbolikoa symbolinen linkki tykislig leinkja lien symbolique nasc siombalach ligazón simbólica קישור סימבולי simbolička veza szimbolikus link Ligamine symbolic taut simbolik Collegamento simbolico シンボリックリンク სიმბოლური ბმული символдық сілтеме 심볼릭 링크 simbolinė nuoroda simboliskā saite Pautan simbolik symbolsk lenke symbolische koppeling symbolsk lenkje ligam simbolic Dowiązanie symboliczne ligação simbólica Ligação simbólica legătură simbolică символьная ссылка Symbolický odkaz simbolna povezava Lidhje simbolike симболичка веза symbolisk länk sembolik bağlantı символічне посилання liên kết tượng trưng 符号链接 符號鏈結 mail delivery report تقرير تسليم البريد poçt yollama raportu rapart ab dastaŭcy pošty Отчет за пристигналата поща informe de lliurament de correu zpráva o doručení pošty Adroddiad trosgludo post postleveringsrapport E-Mail-Zustellungsbericht Αναφορά παράδοσης μηνύματος mail delivery report raporto pri transdono de retpoŝto informe de entrega de correo posta banaketako txostena viestin jakeluilmoitus post útberingarfrásøgn rapport de livraison de courriels tuairisc sheachadadh poist informe de entrega de correo דוח העברת דואר izvještaj dostave pošte jelentés levélkézbesítésről Reporto de livration de e-mail laporan pengantaran surat Rapporto di consegna posta メール配送ポート пошта жеткізілгені туралы отчет 메일 배달 보고서 pašto pristatymo ataskaita pasta piegādes atskaite Laporan penghantaran mel e-postleveranserapport e-mail-bezorgingsbericht e-post-leveringsrapport rapòrt de liurason de corrièrs electronics Raport z dostarczenia poczty relatório de entrega de email Relatório de entrega de correspondência raport de trimitere email отчёт о доставке сообщения Správa o doručení pošty poročilo dostave pošte Raport mbi dorëzimin e mesazhit извештај доставе поруке e-postleveransrapport posta iletim raporu звіт про доставку пошти thông báo phát thư 邮件投递报告 郵件寄送回報 mail disposition report تقرير ترتيب البريد poçt qayıtma raportu rapart ab raźmiaščeńni pošty Отчет за състоянието на пощата informe de disposició de correu zpráva o předání pošty adroddiad ffurf post postdisponeringsrapport E-Mail-Übertragungsbericht Αναφορά διάθεσης μηνύματος mail disposition report raporto pri dispono de retpoŝto informe de disposición de correo posta joerako txostena viestin kuittausilmoitus post avhendingarfrásøgn rapport de disposition de courriels tuairisc chóiriú poist informe de disposición de correo דוח אספקת דואר Izvještaj smještaja e-pošte jelentés levélkidobásról Reporto de disposition de e-mail laporan disposisi surat Rapporto di disposizione posta メール停止レポート пошта жылжытылғаны туралы отчет 메일 처리 보고서 pašto charakteristikos ataskaita pasta izvietojuma atskaite Laporan pelupusan mel e-postdispositionsrapport e-mail-plaatsingsbericht e-post-disposisjonsrapport rapòrt de disposicion de corrièrs electronics Raport z wysyłania poczty relatório de disposição de email Relatório de disposição de correspondência confirmare primire email отчёт о перемещении почты Správa o odovzdaní pošty poročilo razporeditve pošte Raport mbi njoftimin e mesazhit извештај слања поруке e-postdispositionsrapport posta silinme raporu звіт про розташування пошти thông báo chuyển nhượng thư 邮件接收报告 郵件處置回報 reference to remote file مرجع إلى ملف بعيد uzaq fayla göstəriş spasyłka da addalenaha fajłu Препратка към отдалечен файл referència a fitxer remot odkaz na vzdálený soubor cyfeiriad at ffeil bell reference til fjern fil Verweis auf entfernte Datei Αναφορά σε απομακρυσμένο αρχείο reference to remote file referenco al fora dosiero referencia a un archivo remoto erreferentzia urruneko fitxategiari viittaus etätiedostoon tilvísing til fjarfílu référence au fichier distant tagairt do chomhad cianda referencia a un ficheiro remoto התיחסות לקובץ מרוחק referenca na udaljenu datoteku hivatkozás távoli fájlra Referentia a un file remote referensi ke berkas jarak jauh Riferimento a file remoto リモートファイルへの参照 қашықтағы файлға сілтеме 원격 파일 참조 nuoroda į nutolusį failą norāde uz attālinātu datni Rujukan ke fail jauh referanse til ekstern fil verwijzing naar bestand op afstand referanse til fil over nettverk referéncia al fichièr distant Odwołanie do pliku zdalnego referência a um ficheiro remoto Referência para arquivo remoto referință fișier la distanță ссылка на удалённый файл Odkaz na vzdialený súbor sklic do oddaljene datoteke Referim për tek file në distancë упута на удаљену датотеку referens till fjärrfil uzaktaki dosyaya başvuru посилання на віддалений файл tham chiếu đến tập tin ở xa 到远程文件的引用 遠端檔案的參照 Usenet news message رسالة أخبار Usenet Usenet xəbərlər ismarışı Navina Usenet Съобщение — Usenet missatge de notícies Usenet příspěvek do diskusních skupin Usenet Neges newyddion Usenet Usenetnyhedsmeddelelse Usenet-News-Nachricht Μήνυμα ομάδων συζητήσεων Usenet Usenet news message novaĵmesaĝo de Usenet mensaje de noticias de Usenet Usenet berrien mezua nyyssiviesti Usenet news boð message de groupe d'échange Usenet teachtaireacht nuacht Usenet mensaxes de noticias de Usenet הודעת חדשות של Usenet Usenet poruka novosti USENET-hírcsoportüzenet Message de gruppo Usenet Pesan berita Usenet Messaggio news Usenet Usenet news メッセージ Usenet жаңалық мәлімдемесі 유즈넷 뉴스 메시지 Usenet naujienų žinutė Usenet jaunumu ziņojums Mesej berita USENET Usenet nyhetsmelding Usenet-nieuwsbericht USENET diskusjonsmelding messatge de grop d'escambi Usenet Wiadomość grupy dyskusyjnej mensagem de notícias Usenet Mensagem de notícias da Usenet Mesaj Usenet de știri новостное сообщение Usenet Príspevok do diskusných skupín Usenet novičarsko sporočilo Usenet Mesazh lajmesh Usenet Порука новости Јузнета Usenet-diskussionsgruppsmeddelande Usenet haber iletisi повідомлення новин Usenet Thông điệp tin tức USENET Usenet 新闻信 Usenet 新聞訊息 partial email message رسالة البريد الإلكتروني الجزئية qismi poçt ismarışı niapoŭny list email Част от електронно писмо missatge de correu electrònic parcial částečná e-mailová zpráva darn o neges e-bost delvis postmeddelelse E-Mail-Nachrichtenfragment Τμηματικό ηλ. μήνυμα partial email message parta retpoŝta mesaĝo mensaje de correo electrónico parcial posta mezu partziala osittainen sähköpostiviesti message partiel de courriel teachtaireacht ríomhphoist neamhiomlán mensaxe de correo electrónico parcial מסר דוא״ל חלקי djelomična poruka e-pošte részleges elektronikus levél Message de e-mail partial pesan email sebagian Messaggio email parziale 部分メールメッセージ электронды поштаның үзінді мәлімдемесі 부분적 전자 우편 메시지 nepilnas el. laiškas daļēja e-pasta vēstule Bahagian mesej emel del av e-postmelding gedeeltelijk e-mailbericht del av e-post-melding messatge parcial de corrièr electronic Częściowa wiadomość e-mail mensagem parcial de email Mensagem de e-mail parcial mesaj de email parțial фрагмент сообщения электронной почты Čiastočná e-mailová správa delno elektronsko sporočilo Mesazh poste i pjesëshëm делимична порука ел. поште del av e-postmeddelande kısmi eposta iletisi часткове поштове повідомлення thư điện tử riêng phần 部分电子邮件 部份電子郵件訊息 email message رسالة البريد الإلكتروني list email Съобщение по електронната поща missatge de correu electrònic e-mailová zpráva postmeddelelse E-Mail-Nachricht Ηλ. μήνυμα email message retpoŝta mesaĝo mensaje de correo electrónico helbide elektronikoen mezua sähköpostiviesti t-post boð message de courriel teachtaireacht ríomhphoist mensaxe de correo electrónico הודעת דואר אלקטרוני poruka e-pošte elektronikus levél Message de e-mail pesan email Messaggio email メール本文 пошталық мәлімдеме 전자 우편 본문 el. laiškas e-pasta vēstule Mesej emel e-postmelding e-mailbericht e-postmelding messatge de corrièr electronic Wiadomość e-mail mensagem de email Mensagem de e-mail mesaj email почтовое сообщение E-mailová správa sporočilo elektronske pošte Mesazh poste порука ел. поште e-postmeddelande eposta iletisi повідомлення email thư điện tử 电子邮件 電子郵件內容 GNU mail message رسالة بريد جنو GNU poçt ismarışı List GNU Съобщение — GNU mail missatge de GNU mail zpráva GNU mail Neges E-Bost GNU GNU-postmeddelelse GNU-Mail-Nachricht Μήνυμα αλληλογραφίας GNU GNU mail message mesaĝo de GNU mail mensaje de correo de GNU GNU posta mezua GNU-postiviesti GNU mail boð message de courriel GNU teachtaireacht phost GNU mensaxe de correo electrónico de GNU הודעת דואר של GNU GNU poruka pošte GNU elektronikus levél Message electronic de GNU Pesan surat GNU Messaggio GNU mail GNU メールメッセージ GNU mail შეტყობინება GNU пошта хабарламасы GNU 메일 메시지 GNU pašto žinutė GNU pasta vēstule Mesej emel GNU GNU e-postmelding GNU-mailbericht GNU e-postmelding messatge de corrièr electronic GNU Wiadomość pocztowa GNU mensagem de email GNU Mensagem de e-mail GNU Mesaj GNU mail почтовое сообщение GNU Správa GNU mail Sporočilo pošte GNU Mesazh GNU mail порука Гнуове поште GNU-epostmeddelande GNU posta iletisi поштове повідомлення GNU Thư điện tử của GNU GNU mail 信件 GNU 郵件訊息 IGES document IGES Initial Graphics Exchange Specification VRML document مستند VRML VRML sənədi Dakument VRML Документ — VRML document VRML dokument VRML Dogfen VRML VRML-dokument VRML-Dokument Έγγραφο VRML VRML document VRML-dokumento documento VRML VRML dokumentua VRML-asiakirja VRML skjal document VRML cáipéis VRML documento VRML מסמך VRML VRML dokument VRML-dokumentum Documento VRML Dokumen VRML Documento VRML VRML ドキュメント VRML құжаты VRML 문서 VRML dokumentas VRML dokuments Dokumen VRML VRML-dokument VRML-document VRML-dokument document VRML Dokument VRML documento VRML Documento VRML Document VRML документ VRML Dokument VRML Dokument VRML Dokument VRML ВРМЛ документ VRML-dokument VRML belgesi документ VRML Tài liệu VRML VRML 文档 VRML 文件 VRML Virtual Reality Modeling Language message in several formats رسالة في عدة صيغ verici formatlarında ismarış paviedamleńnie ŭ niekalkich farmatach Съобщение в няколко формата missatge en diversos formats zpráva v několika formátech neges mewn sawl fformat meddelelse i flere formater Nachricht in mehreren Formaten Μήνυμα σε διάφορες μορφές message in several formats mesaĝo en pluraj formatoj mensaje en varios formatos hainbat formatuko mezua viesti useissa muodoissa boð í fleiri sniðum message en formats divers teachtaireacht i roinnt fhormáidí mensaxe en varios formatos הודעה במספר תבניות poruka u nekoliko oblika többféle formátumú üzenet Message in plure formatos pesan dalam beberapa format Messaggio in diversi formati いくつかの形式でのメッセージ бірнеше пішімдегі мәлімдеме 여러가지 형식의 메시지 laiškas keletu formatų ziņojums dažādos formātos Mesej dalam beberapa format melding i flere formater bericht in meerdere opmaken melding i fleire format messatge en formats divèrses Wiadomość w wielu formatach mensagem em vários formatos Mensagem em vários formatos mesaj în diferite formate сообщение в нескольких форматах Správa v niekoľkých formátoch sporočilo v več zapisih Mesazh në formate të ndryshëm порука у неколико записа meddelande i flera format farklı biçimlerde ileti повідомлення у кількох форматах thông điệp có vài định dạng 各种格式的消息 多種格式的訊息 Macintosh AppleDouble-encoded file ملف Macintosh AppleDouble مشفر Macintosh AppleDouble-kodlanmış fayl Fajł Macintosh, AppleDouble-zakadavany Файл — кодиран с Macintosh AppleDouble fitxer codificat AppleDouble de Macintosh soubor kódovaný pomocí Macintosh AppleDouble Ffeil AppleDouble-amgodedig Macintosh Macintosh AppleDouble-kodet fil Macintosh-Datei (AppleDouble-kodiert) Αρχείο Macintosh κωδικοποίησης AppleDouble Macintosh AppleDouble-encoded file dosiero kodigita laŭ Macintosh AppleDouble archivo Macintosh codificado con AppleDouble Macintosh AppleDouble-rekin kodetutako fitxategia Macintosh AppleDouble -koodattu tiedosto Macintosh AppleDouble-bronglað fíla fichier codé Macintosh AppleDouble comhad ionchódaithe le Macintosh AppleDouble ficheiro de Macintosh codificado con AppleDouble קובץ מסוג Macintosh AppleDouble-encoded Macintosh AppleDouble-kodirana datoteka Macintosh AppleDouble kódolású fájl File codificate in AppleDouble de Macintosh Berkas tersandi Macintosh AppleDouble File Macintosh codificato AppleDouble Macintosh AppleDouble エンコードファイル Macintosh AppleDouble кодталған файлы 매킨토시 AppleDouble 인코딩된 파일 Macintosh AppleDouble-encoded failas Macintosh AppleDouble-kodēts datne Fail terenkod-AppleDouble Macintosh dokument kodet med Macintosh AppleDouble Macintosh AppleDouble-gecodeerd bestand Macintosh AppleDouble-koda fil fichièr encodat Macintosh AppleDouble Zakodowany w AppleDouble plik Macintosh ficheiro codificado em AppleDouble de Macintosh Arquivo do Macintosh codificado com AppleDouble Fișier codat Macintosh AppleDouble файл (закодированный Macintosh AppleDouble) Súbor kódovaný pomocou Macintosh AppleDouble Kodirana datoteka Macintosh (AppleDouble) File Macintosh i kodifikuar AppleDouble Мекинтошова датотека кодирана Епл Дуплим Macintosh AppleDouble-kodad fil Macintosh AppleDouble-şifreli dosyası файл закодований Macintosh AppleDouble Tập tin đã mã hoá Apple-Double của Macintosh Macintosh AppleDouble 编码的文件 Macintosh AppleDouble 編碼檔 message digest خلاصة الرسالة ismarış daycesti digest paviedamleńniaŭ Извадка от съобщение recopilació de missatges přehled zpráv crynodeb negeseuon meddelelsessammendrag Nachrichtensammlung Περίληψη μηνύματος message digest mesaĝaro recopilación de mensajes mezu laburra viestikokoelma boð samandráttur condensé de message achoimre theachtaireachtaí recompilación de mensaxe תקציר ההודעה Poruka kratkg sadržaja ömlesztett üzenet Digesto de messages pesan digest Digest di messaggi メッセージダイジェスト мәлімдеме профилі 메시지 묶음 laiškų santrauka ziņojumu apkopojums Jilid mesej medldingssamling berichtenbundel meldingsamandrag condensé de messatge Wiadomość przetwarzania grupo de mensagens Resumo de mensagem colecție mesaje email профиль сообщения Prehľad správ povzetek sporočila Shpërndarje mesazhesh гомила порука meddelandesamling mesaj özeti збірка повідомлень bản tóm tắt thông điệp 消息摘要 訊息摘要 encrypted message رسالة مشفرة şifrələnmiş ismarış zašyfravanaje paviedamleńnie Шифрирано съобщение missatge xifrat zašifrovaná zpráva Neges wedi ei hamgryptio krypteret meddelelse Verschlüsselte Nachricht Κρυπτογραφημένο μήνυμα encrypted message ĉifrita mesaĝo mensaje cifrado mezu enkriptatua salattu viesti bronglað boð message chiffré teachtaireacht chriptithe mensaxe cifrado הודעה מוצפנת šifrirana poruka titkosított üzenet Message cryptate pesan terenkripsi Messaggio cifrato 暗号化メッセージ шифрленген мәлімдеме 암호화된 메시지 užšifruotas laiškas šifrēta vēstule Mesej terenkripsi kryptert melding versleuteld bericht kryptert melding messatge chifrat Wiadomość zaszyfrowana mensagem encriptada Mensagem criptografada mesaj criptat зашифрованное сообщение Zašifrovaná správa šifrirano sporočilo Mesazh i kriptuar шифрована порука krypterat meddelande şifrelenmiş mesaj шифроване повідомлення thông điệp đã mật mã 加密信件 加密訊息 compound documents مستندات مركبة składanyja dakumenty Съставни документи documents compostos složené dokumenty sammensatte dokumenter Verbunddokumente Σύνθετα έγγραφα compound documents parentezaj dokumentoj documentos compuestos konposatutako dokumentuak yhdisteasiakirjat samansett skjøl documents composés cáipéisí comhshuite documentos compostos מסמכים מורכבים Složeni dokumenti összetett dokumentumok Documentos composite dokumen kompon Documenti composti 複合ドキュメント құрама құжаттары 복합 문서 sudurtiniai dokumentai salikti dokumenti Dokumen halaman sammensatte dokumenter samengestelde documenten samansette dokument documents compausats Dokumenty złożone documentos compostos Documentos compostos documente compuse составные документы Zložené dokumenty združeni dokumenti dokumente të përbërë сједињени документи sammansatta dokument birleşik belgeleri складні документи tài liệu ghép 组合文档 複合文件 compound document مستند مركب birləşik sənəd składany dakument Съставен документ document compost složený dokument dogfen gyfansawdd sammensat dokument Verbunddokument Σύνθετο έγγραφο compound document parenteza dokumento documento compuesto konposatutako dokumentua yhdisteasiakirja samansett skjal document composé cáipéis comhshuite documento composto מסמך מורכב Složeni dokument összetett dokumentum Documento composite dokumen kompon Documento composto 複合ドキュメント құрама құжаты 복합 문서 sudurtinis dokumentas salikts dokuments Dokumen halaman sammensatt dokument samengesteld document samansett dokument document compausat Dokument złożony documento composto Documento composto document compus составной документ Zložený dokument združeni dokument dokumet i përbërë сједињени документ sammansatt dokument bileşik belge складний документ tài liệu ghép 组合文档 複合文件 mail system report تقرير نظام البريد poçt sistemi raportu rapart paštovaj systemy Отчет за пощенската система informe de sistema de correu zpráva poštovního systému adroddiad system bost postsystemrapport E-Mail-Systembericht Αναφορά συστήματος ηλ. ταχυδρομείου mail system report raporto de retpoŝta sistemo informe del sistema de correo posta sistemako txostena viestijärjestelmän ilmoitus postkervisfrásøgn rapport système de courriels tuairisc chóras poist informe do sistema de correo דו״ח של מערכת הדואר Izvještaj sustava pošte levelezőrendszer jelentése Reporto de systema de e-mail laporan sistem surat Rapporto di sistema posta メールシステムレポート пошта жүйесінің мәлімдемесі 메일 시스템 보고서 pašto sistemos ataskaita pasta sistēmas atskaite Laporan sistem mel e-postsystemrapport e-mail-systeembericht e-post-systemrapport rapòrt sistèma de corrièrs electronics Raport systemu pocztowego relatório de sistema de email Relatório do sistema de correspondência raport sistem email отчёт почтовой системы Správa poštového systému poročilo poštnega sistema Raport i sistemit të postës извештај поштанског система e-postsystemrapport posta sistem raporu звіт поштової системи thông báo hệ thống thư 邮件系统报告 郵件系統回報 signed message رسالة موقّعة imzalanmış ismarış padpisanaje paviedamleńnie Подписано съобщение missatge signat podepsaná zpráva neges lofnodwyd signeret meddelelse Signierte Nachricht Υπογεγραμμένο μήνυμα signed message pruvita mesaĝo mensaje firmado sinatutako mezua allekirjoitettu viesti undirskrivað boð message signé teachtaireacht sínithe mensaxe firmado הודעה חתומה potpisana poruka aláírt üzenet Message signate pesan ditandatangani Messaggio firmato 署名付きメッセージ қолтаңбасы бар мәлімдеме 서명된 메시지 pasirašytas laiškas parakstīta ziņa Mesej ditandatangani signert melding ondertekend bericht signert melding messatge signat Podpisana wiadomość mensagem assinada Mensagem assinada mesaj semnat подписанное сообщение Podpísaná správa podpisano sporočilo Mesazh i firmosur потписана порука signerat meddelande imzalı ileti підписане повідомлення thông điệp đã ký 签名信件 簽署的訊息 stream of data (server push) دفق بيانات (دفع خادم) płyń źviestak (ad servera) Поток от данни, от страна на сървър flux de dades (enviat pel servidor) proud dat (posílaný serverem) datastrøm (serverskubbet) Datenstrom (Server-Push) Ροή δεδομένων (στελλόμενα από διακομιστή) stream of data (server push) datumstrio (puŝata per servilo) flujo de datos (por iniciativa del servidor) datu-korrontea (zerbitzari igortzailea) tietovirta (palvelin työntää) streymur av dáta (ambætara skump) flux de données (émis par le serveur) sruth sonraí (brú freastalaí) fluxo de datos (por iniciativa do servidor) מידע בזרימה (דחיפה ע״י השרת) Tok podataka (poslužiteljem pogurano) sugárzott adatfolyam (kiszolgálóról) Fluxo de datos (pulsate per servitor) arus data (dorongan server) Flusso di dati (server push) データストリーム (サーバープッシュ型) мәліметтер ағымы (server push) 데이터 스트림(서버 푸시) duomenų srautas (iš serverio) datu straume (servera grūsta) Aliran dara (paksaan pelayan) datastrøm (server push) gegevensstroom (server duwt) datastraum (dytta av tenaren) flux de donadas (emés pel servidor) Strumień danych (wymuszenie serwera) fluxo de dados (empurrados pelo servidor) Fluxo de dados (por iniciativa do servidor) flux de date (de la server) поток данных (server push) Prúd dát (posielaný serverom) pretok podatkov (strežniški) Fluks me të dhëna (server push) ток података (гурање са сервера) dataflöde (serverutsändning) veri akışı (sunucudan gönderilen) потік даних (від сервера) luồng dữ liệu (trình phục vụ đẩy) 数据流(服务器推送) 資料串流 (server push) VCS/ICS calendar سجل VCS/ICS Kalandar VCS/ICS Календар — VCS/ICS calendari VCS/ICS kalendář VCS/ICS VCS/ICS-kalender VCS/ICS-Kalender Ημερολόγιο VCS/ICS VCS/ICS calendar VCS/ICS-kalendaro calendario VCS/ICS VCS/ICS egutegia VCS/ICS-kalenteri VCS/ICS kalendari calendrier VCS/ICS féilire VCS/ICS Calendario VCS/ICS לוח שנה VCS/ICS VCS/ICS kalendar VCS/ICS naptár Calendario VCS/ICS Kalender VCS/ICS Calendario VCS/ICS VCS/ICS カレンダー VCS/ICS күнтізбесі VCS/ICS 달력 VCS/ICS kalendorius VCS/ICS kalendārs VCS/ICS-kalender VCS/ICS-kalender VCS/ICS-kalender calendièr VCS/ICS Kalendarz VCS/ICS calendário VCS/ICS Calendário VCS/ICS Calendar VCS/ICS календарь VCS/ICS Kalendár VCS/ICS Datoteka koledarja VCS/ICS Kalendar VCS/ICS ВЦС/ИЦС календар VCS/ICS-kalender VCS/ICS takvimi календар VCS/ICS Lịch VCS/ICS VCS/ICS 日历 VCS/ICS 行事曆 VCS/ICS vCalendar/iCalendar CSS stylesheet نمط CSS Arkuš stylaŭ CSS Стилове — CSS llista d'estil CSS stylopis CSS CSS-stilark CSS-Stilvorlage Φύλλο στυλ CSS CSS stylesheet CSS-stilfolio hoja de estilos CSS CSS estilo-orria CSS-tyylitiedosto CSS sniðark feuille de style CSS stílbhileog CSS folla de estilos CSS גליון עיצוב CSS CSS stilska tablica CSS stíluslap Folio de stilo CSS Lembar gaya CSS Foglio di stile CSS CSS スタイルシート CSS სტილი CSS стильдер кестесі CSS 스타일시트 CSS stiliaus aprašas CSS stilu saraksts CSS-stilark CSS-stijlblad CSS-stilark fuèlh d'estil CSS Arkusz stylów CSS folha de estilos CSS Folha de estilo CSS Pagină de stil CSS таблица стилей CSS Štýly CSS Slogovna predloga CSS Fletë stili CSS ЦСС стилски лист CSS-stilmall CSS stil kağıdı таблиця стилів CSS Tờ kiểu dáng CSS CSS 样式表 CSS 樣式表 CSS Cascading Style Sheets electronic business card بطاقة أعمال إلكترونية elektronnaja biznes-kartka Електронна визитна картичка targeta de visita electrònica elektronická navštívenka elektronisk visitkort Elektronische Visitenkarte Ηλεκτρονική επαγγελματική κάρτα electronic business card elektronika vizitkarto tarjeta de visita electrónica enpresako txartel elektronikoa sähköinen käyntikortti elektroniskt handilskort carte de visite électronique cárta gnó leictreonach tarxeta de negocio electrónica כרטיס ביקור אלקטרוני elektronička posjetnica elektronikus névjegykártya Carta de visita electronic kartu bisnis elektronik Biglietto da visita elettronico 電子名刺 электронды визит карточкасы 전자 명함 elektroninė vizitinė kortelė elektroniskā biznesa kartiņa elektronisch visitekaartje elektronisk visittkort carta de visita electronica Wizytówka elektroniczna cartão de visita eletrónico Cartão de visitas eletrônico carte de vizită electronică электронная визитная карточка Elektronická vizitka elektronska poslovna vizitka Skedë elektronike biznesi електронска пословна картица elektroniskt visitkort elektronik iş kartı електронна бізнес-картка danh thiếp điện tử 电子商务卡 電子商務名片 Turtle document document Turtle dokument Turtle Turtle-dokument Turtle-Dokument Έγγραφο Turtle Turtle document documento de Turtle Turtle dokumentua Turtle-asiakirja Turtle dokument Turtle dokumentum Documento Turtle Dokumen Turtle Documento Turtle Turtle құжаты Turtle 문서 document Turtle Dokument Turtle documento Turtle Documento Turtle документ Turtle Dokument Turtle Тартл документ Turtle-dokument Turtle belgesi документ Turtle Turtle 文档 Turtle 文件 txt2tags document مستند txt2tags dakument txt2tags Документ — txt2tags document txt2tags dokument txt2tags txt2tags-dokument txt2tags-Dokument Έγγραφο txt2tags txt2tags document txt2tags-dokumento documento txt2tags txt2tags dokumentua txt2tags-asiakirja txt2tags skjal document txt2tags cáipéis txt2tags documento txt2tags מסמך txt2tags txt2tags dokument txt2tags dokumentum Documento txt2tags dokumen txt2tags Documento txt2tags txt2tags ドキュメント txt2tags დოკუმენტი txt2tags құжаты txt2tags 문서 txt2tags dokumentas txt2tags dokuments txt2tags-dokument txt2tags-document txt2tags-dokument document txt2tags Dokument txt2tags documento txt2tags Documento do txt2tags document txt2tags документ txt2tags Dokument txt2tags Dokument txt2tags Dokument txt2tags документ текста-у-ознаке txt2tags-dokument txt2tags belgesi документ txt2tags tài liệu txt2tags txt2tags 文档 txt2tags 文件 Verilog source code Изходен код — Verilog codi font en Verilog zdrojový kód Verilog Verilog-kildekode Verilog-Quelltext Πηγαίος κώδικας Verilog Verilog source code Verilog-fontkodo código fuente en Verilog Verilog iturburu-kodea Verilog-lähdekoodi code source Verilog código fonte en Verilog קוד מקור של Verilog izvorni kod Verilog-forráskód Codice-fonte Verilog Kode sumber Verilog Codice sorgente Verilog Verilog ソースコード Verilog бастапқы коды Verilog 소스 코드 Verilog pirmkods Verilog broncode còde font Verilog Kod źródłowy Verilog código origem Verilog Código-fonte Verilog исходный код Verilog Zdrojový kód Verilog Datoteka izvorne kode Verilog изворни код Верилога Verilog-källkod Verilog kaynak kodu вихідний код мовою Verilog Verilog 源代码 Verilog 源碼 SystemVerilog header Заглавен файл — SystemVerilog capçalera de SystemVerilog záhlaví SystemVerilog SystemVerilog-teksthoved SystemVerilog-Header Κεφαλίδα SystemVerilog SystemVerilog header cabeceras de SystemVerilog SystemVerilog goiburua SystemVerilog-otsake en-tête Cabeceiras de SystemVerilog כותרת SystemVerilog SystemVerilog zaglavlje SystemVerilog fejléc Capite SystemVerilog Header SystemVerilog Header SystemVerilog SystemVerilog ヘッダー SystemVerilog тақырыптамасы SystemVerilog 헤더 SystemVerilog galvene SystemVerilog header entèsta SystemVerilog Nagłówek SystemVerilog cabeçalho SystemVerilog Cabeçalho de SystemVerilog заголовочный файл SystemVerilog Hlavičky SystemVerilog Datoteka glave SystemVerilog заглавље Система Верилога SystemVerilog-headerfil SystemVerilog başlığı заголовки SystemVerilog SystemVerilog 头 SystemVerilog 標頭 SystemVerilog source code Изходен код — SystemVerilog codi font en SystemVerilog zdrojový kód SystemVerilog SystemVerilog-kildekode SystemVerilog-Quelltext Πηγαίος κώδικας SystemVerilog SystemVerilog source code código fuente en SystemVerilog SystemVerilog iturburu-kodea SystemVerilog-lähdekoodi code source código fonte en SystemVerilog קוד מקור של SystemVerilog SystemVerilog izvorni kod SystemVerilog-forráskód Codice-fonte SystemVerilog Kode sumber SystemVerilog Codice sorgente SystemVerilog ソースコード SystemVerilog бастапқы коды SystemVerilog 소스 코드 SystemVerilog pirmkods SystemVerilog broncode còde font SystemVerilog Kod źródłowy SystemVerilog código origem SystemVerilog Código-fonte de SystemVerilog исходный код SystemVerilog Zdrojový kód SystemVerilog Datoteka izvorne kode SystemVerilog изворни код Система Верилога SystemVerilog-källkod SystemVerilog kaynak kodu вихідний файл мовою SystemVerilog SystemVerilog 源代码 SystemVerilog 源碼 VHDL source code Изходен код — VHDL codi font en VHDL zdrojový kód VHDL VHDL-kildekode VHDL-Quelltext Πηγαίος κώδικας VHDL VHDL source code VHDL-fontkodo código fuente en VHDL VHDL iturburu-kodea VHDL-lähdekoodi code source VHDL código fonte en VHDL קוד מקור של VHDL VHDL izvorni kod VHDL-forráskód Codice-fonte VHDL Kode sumber VHDL Codice sorgente VHDL VHDL ソースコード VHDL бастапқы коды VHDL 소스 코드 VHDL pirmkods VHDL broncode còde font VHDL Kod źródłowy VHDL código origem VHDL Código-fonte VHDL исходный код VHDL Zdrojový kód VHDL Datoteka izvorne kode VHDL ВХДЛ изворни код VHDL-källkod VHDL kaynak kodu вихідний код мовою VHDL VHDL 源代码 VHDL 源碼 VHDL Very-High-Speed Integrated Circuit Hardware Description Language enriched text document مستند نصي مغنى zəngin mətn sənədi azdobleny tekstavy dakument Документ с обогатен текст document de text enriquit rozšířený textový dokument Dogfen testun wedi ei gyfoethogi beriget tekstdokument Angereichertes Textdokument Έγγραφο εμπλουτισμένου κειμένου enriched text document riĉigita teksta dokumento documento de texto enriquecido aberastutako testu dokumentua rikastettu tekstiasiakirja ríkað tekstskjal document texte enrichi cáipéis téacs saibhrithe documento de texto enriquecido מסמך טקסט מועשר obogaćeni tekstualni dokument enriched text dokumentum Documento de texto inricchite dokumen teks diperkaya Documento testo arricchito リッチテキストドキュメント пішімделген мәтіндік құжаты 확장된 텍스트 문서 praturtinto teksto dokumentas bagātināta teksta formāts Dokumen teks diperkaya riktekst-dokument verrijkt tekstdocument rik tekst tekstdokument document tèxte enriquit Wzbogacony dokument tekstowy documento de texto rico Documento de texto enriquecido document text îmbogățit форматированный текстовый документ Rozšírený textový dokument dokument z obogatenim besedilom Dokument teksti i pasuruar обогаћени текстуални документ berikat textdokument zenginleştirilmiş metin belgesi форматований текстовий документ tài liệu văn bản có kiểu dáng 富文本文档 豐富化文字文件 help page صفحة المساعدة yardım səhifəsi staronka dapamohi Страница от помощта pàgina d'ajuda stránka nápovědy tudalen gymorth hjælpeside Hilfeseite Σελίδα βοήθειας help page help-paĝo página de ayuda laguntzako orria ohjesivu hjálparsíða page d'aide leathanach cabhrach páxina de axuda דף עזרה stranica pomoći súgóoldal Pagina de adjuta halaman bantuan Pagina di aiuto ヘルプページ анықтама парағы 도움말 페이지 žinyno puslapis palīdzības lapa Halaman bantuan hjelpside hulppagina hjelpeside pagina d'ajuda Strona pomocy página de ajuda Página de ajuda pagină de ajutor страница справки Stránka Pomocníka stran pomoči Faqe ndihme страница помоћи hjälpsida yardım sayfası сторінка довідки trang trợ giúp 帮助页面 求助頁面 plain text document مستند نصي مجرد prosty tekstavy dakument Документ с неформатиран текст document de text pla prostý textový dokument rent tekstdokument Einfaches Textdokument Έγγραφο απλού κειμένου plain text document plata teksta dokumento documento de texto sencillo testu soileko dokumentua perustekstiasiakirja document texte brut cáipéis ghnáth-théacs documento de texto sinxelo מסמך טקסט פשוט običan tekstualni dokument egyszerű szöveg Documento de texto simple dokumen teks biasa Documento in testo semplice 平文テキストドキュメント мәтіндік құжаты 일반 텍스트 문서 paprastas tekstinis dokumentas vienkāršs teksta dokuments Dokumen teks jernih vanlig tekstdokument plattetekst-document vanleg tekstdokument document tèxte brut Zwykły dokument tekstowy documento em texto simples Documento de Texto document text simplu текстовый документ Obyčajný textový dokument običajna besedilna datoteka Dokument në tekst të thjeshtë обичан текстуални документ vanligt textdokument düz metin belgesi звичайний текстовий документ tài liệu nhập thô 纯文本文档 純文字文件 RDF file ملف RDF Fajł RDF Файл — RDF fitxer RDF soubor RDF RDF-fil RDF-Datei Αρχείο RDF RDF file RDF-dosiero archivo RDF RDF fitxategia RDF-tiedosto RDF fíla fichier RDF comhad RDF ficheiro RDF קובץ RDF RDF datoteka RDF fájl File RDF Arsip RDF File RDF RDF ファイル RDF файлы RDF 파일 RDF failas RDF datne RDF-fil RDF-bestand RDF-fil fichièr RDF Plik RDF ficheiro RDF Arquivo RDF Fișier RDF файл RDF Súbor RDF Datoteka RDF File RDF РДФ датотека RDF-fil RDF dosyası файл RDF Tập tin RDF RDF 文件 RDF 檔 RDF Resource Description Framework OWL XML file fitxer XML OWL soubor OWL XML OWL XML-fil OWL-XML-Datei Αρχείο OWL XML OWL XML file archivo en XML OWL OWL XML fitxategia OWL XML datoteka OWL XML-fájl File XML OWL Berkas XML OWL File XML OWL OWL XML файлы OWL XML 파일 fichièr OWL XML Plik XML OWL ficheiro OWL XML Arquivo OWL XML файл XML OWL Súbor XML OWL ОВЛ ИксМЛ датотека OWL XML-fil OWL XML dosyası файл XML OWL OWL XML 文件 OWL XML 檔案 OWL Web Ontology Language email headers ترويسة البريد الإلكتروني epoçt başlıqları paštovyja zahałoŭki Заглавни части на електронни писма capçaleres de correu electrònic záhlaví e-mailu penawdau e-bost posthoveder E-Mail-Kopfzeilen Κεφαλίδες ηλ. μηνυμάτων email headers retpoŝtaj ĉapoj cabeceras de correo electrónico helbide elektronikoen goiburuak sähköpostiotsakkeet t-post tekshøvd en-têtes de courriel ceanntásca ríomhphoist cabeceiras de correo electrónico כותרת דוא״ל zaglavlja e-pošte levélfejléc Capites de e-mail tajuk email Intestazioni email メールヘッダー пошталық тақырыптамалары 전자 우편 헤더 el. laiško antraštės e-pasta galvene Pengepala emel e-posthode e-mail-kopregels e-post-hovud entèstas de corrièr electronic Nagłówki wiadomości e-mail cabeçalhos de email Cabeçalhos de e-mail antete email почтовые заголовки Hlavičky e-mailu glava elektronske pošte Header email заглавља ел. поште e-posthuvuden eposta başlığı заголовки email dòng đầu thư điện tử 电子邮件头 電子郵件標頭 rich text document مستند نصي غني zəngin mətn sənədi azdobleny tekstavy dakument Документ — rich text document de text enriquit textový dokument RTF dogfen testun gyfoethog (rtf) richtekstdokument RTF-Textdokument Έγγραφο εμπλουτισμένου κειμένου (RTF) rich text document riĉteksta dokumento documento de texto enriquecido aberastutako testu formatua RTF-asiakirja document « rich text » cáipéis mhéith-théacs documento do texto enriquecido מסמך טקסט עשיר obogaćeni tekstualni dokument rich text-dokumentum Documento de texto inricchite dokumen teks kaya Documento rich text リッチテキストドキュメント пішімделген мәтіні бар құжаты 서식 있는 텍스트 문서 praturtinto teksto dokumentas bagātā teksta dokuments Dokumen teks diperkaya rik tekst-dokument opgemaakt tekstdocument rik tekst-dokument document « rich text » Dokument Rich Text documento em texto rico Documento rich text document text îmbogățit документ с форматированным текстом Textový dokument RTF dokument z oblikovanim besedilom Dokument rich text богат текстуални документ RTF-textdokument zengin metin belgesi форматований текстовий документ tài liệu văn bản có kiểu dáng (RTF) RTF 丰富文本文档 豐富文字文件 RSS summary ملخص RSS Karotki ahlad RSS Обобщение за сайтове — RSS resum RSS souhrn RSS RSS-sammendrag RSS-Zusammenfassung Σύνοψη RSS RSS summary resumen de RSS RSS laburpena RSS-tiivistelmä RSS samandráttur résumé RSS achoimre RSS Resumo RSS תקציר RSS RSS sažetak RSS összefoglaló Summario RSS Ringkasan RSS Sommario RSS RSS サマリ RSS жинақталғаны RSS 요약 RSS santrauka RSS kopsavilkums RSS-sammendrag RSS-samenvatting RSS-samandrag resumit RSS Podsumowanie RSS resumo RSS Resumo RSS Rezumat RSS сводка RSS Súhrn RSS Datoteka povzetek RSS Përmbledhje RSS РСС сажетак RSS-sammanfattning RSS özeti зведення сайту RSS Bản tóm tắt RSS RSS 摘要 RSS 摘要 RSS RDF Site Summary Atom syndication feed مروج تغذية Atom Syndykacyjny kanał navinaŭ Atom Емисия — Atom canal de sindicació Atom kanál Atom Atom syndication-feed Atom-Nachrichtenquelle Τροφοδοσία διανομής Atom Atom syndication feed canal de noticias Atom Atom harpidetze-iturria Atom-yhdistevirta fil de syndication Atom fotha sindeacáitithe Atom fonte de sindicación Atom הזנה דרך הרשת של Atom Atom kanal objavljivanja Atom egyesítőfolyam Fluxo de syndication Atom Umpan sindikasi Atom Feed di distribuzione Atom Atom 配信フィード Atom жаңалықтар таспасы Atom 동기화 피드 Atom sindikacijos kanalas Atom sindikāta barotne Atom syndikeringsstrøm Atom-syndicatie-feed Atom-kjelde fial de sindicacion Atom Kanał Atom feed Atom Fonte de notícias Atom Flux agregare Atom лента новостей Atom Kanál Atom Sindikalni vir Atom Feed për përhapje Atom Атомов довод синдикализације Atom-syndikeringskanal Atom besleme kaynağı трансляція подач Atom Nguồn tin tức Atom Atom 更新种子 Atom 聯合供稿饋流 OPML syndication feed مروج تغذية OPML Syndykacyjny kanał OPML Емисия — OPML canal de sindicació OPML kanál OPML OPML-syndikeringsfeed OPML-Nachrichtenquelle Τροφοδοσία OPML OPML syndication feed canal de noticias OPML OPML harpidetze-iturria OPML-yhdistevirta fil de syndication OPML fotha sindeacáitithe OPML fonte de sindicación OPML הזנה דרך הרשת OPML OPML kanal objavljivanja OPML egyesítőfolyam Fluxo de syndication OPML Umpan sindikasi OPML Feed di distribuzione OPML OPML 配信フィード OPML жаңалықтар таспасы OPML 묶음 피드 OPML sindikacijos kanalas OPML sindikāta barotne OPML syndikeringsstrøm OPML-syndicatie-feed OPML-kjelde fial de sindicacion OPML Kanał OPML feed OPML Fonte de notícias OPML Flux OPML syndication лента новостей OPML Kanál OPML Sindikalni vir OPML Feed për përhapje OPML ОМПЛ довод синдикализације OPML-syndikeringskanal OPML besleme kaynağı трансляція подач OPML Nguồn tin tức OPML OPML 聚合种子 OPML 聯合供稿饋流 SGML document مستند SGML Dakument SGML Документ — SGML document SGML dokument SGML Dogfen SGML SGML-dokument SGML-Dokument Έγγραφο SGML SGML document SGML-dokumento documento SGML SGML dokumentua SGML-asiakirja SGML skjal document SGML cáipéis SGML documento SGML מסמך SGML SGML dokument SGML-dokumentum Documento SGML Dokumen SGML Documento SGML SGML ドキュメント SGML құжаты SGML 문서 SGML dokumentas SGML dokuments Dokumen SGML SGML-dokument SGML-document SGML-dokument document SGML Dokument SGML documento SGML Documento SGML Document SGML документ SGML Dokument SGML Dokument SGML Dokument SGML СГМЛ документ SGML-dokument SGML belgesi документ SGML Tài liệu SGML SGML 文档 SGML 文件 SGML Standard Generalized Markup Language spreadsheet interchange document مستند تبادل الجدول dakument dla abmienu raźlikovymi arkušami Документ за обмяна между програми за електронни таблици document d'intercanvi de full de càlcul sešitový výměnný dokument regnearksudvekslingsdokument Tabellenkalkulations-Austauschdokument Έγγραφο ανταλλαγής λογιστικού φύλλου spreadsheet interchange document documento de intercambio de hojas de cálculo kalkulu-orriak trukatzeko dokumentua taulukkovälitysasiakirja rokniarks umbýtisskjal document d'échange de feuilles de calcul cáipéis idirmhalartaithe scarbhileog documento de intercambio de follas de cálculo מסמך גליון נתונים מתחלף Dokument razmjene proračunske tablice spreadsheet-cserélhetődokumentum Documento de intercambio de folio de calculo dokumen lembar sebar saling tukar Documento di scambio per foglio di calcolo スプレッドシート交換ドキュメント spreadsheet interchange құжаты 스프레드시트 교환 문서 skaičialenčių apsikeitimo dokumentas izklājlapu apmaiņas dokuments dokument for regnearkutveksling rekenblad-uitwisselingsdocument Utvekslingsdokument for rekneark document d'escambi de fuèlhs de calcul Dokument wymiany arkuszy kalkulacyjnych documento de troca interna de folhas de cálculo Documento de intercâmbio de planilhas document schimb filă de calcul документ Spreadsheet Interchange Zošitový prenosový dokument dokument izmenjeve preglednic Dokument shkëmbimi për fletë llogaritje документ размене табеле spreadsheet interchange-dokument hesap tablosu değişim belgesi документ обміну ел. таблицями tài liệu hoán đổi bảng tính 电子表格交换文档 試算表交換文件 TSV document مستند TSV Dakument TSV Документ — TSV document TSV dokument TSV TSV-dokument TSV-Dokument Έγγραφο TSV TSV document documento TSV TSV dokumentua TSV-asiakirja TSV skjal document TSV cáipéis TSV documento TSV מסמך TSV TSV dokument TSV dokumentum Documento TSV Dokumen TSV Documento TSV TSV ドキュメント TSV құжаты TSV 문서 TSV dokumentas TSV dokuments TSV-dokument TSV-document TSV-dokument document TSV Dokument TSV documento TSV Documento TSV Document TSV документ TSV Dokument TSV Dokument TSV Dokument TSV ТСВ документ TSV-dokument TSV belgesi документ TSV Tài liệu TSV TSV 文档 TSV 文件 TSV Tab Separated Values Graphviz DOT graph مبيان Graphviz DOT Граф — Graphviz DOT gràfic Graphviz DOT graf Graphviz DOT Graphviz DOT-graf Graphviz-DOT-Graph Γράφημα Graphviz DOT Graphviz DOT graph gráfico de Graphviz DOT Graphviz DOT grafikoa Graphviz DOT -graafi Graphviz DOT ritmynd graphe Graphviz DOT graf DOT Graphviz gráfica DOT de Graphviz תרשים של Graphviz DOT Graphviz DOT grafikon Graphviz DOT-grafikon Graphico DOT de Graphviz Grafik Graphviz DOT Grafico Graphviz DOT Graphviz DOT グラフ Graphviz DOT сызбасы Graphviz DOT 그래프 Graphviz DOT diagrama Graphviz DOT grafiks Graphviz wetenschappelijke grafiek graf Graphviz DOT Wykres DOT Graphviz gráfico Graphviz DOT Gráfico do Graphviz DOT Grafic Graphviz DOT Диаграмма Graphviz DOT Graf Graphviz DOT Datoteka grafikona Graphviz DOT график Графвиз ДОТ-а Graphviz DOT-graf Graphviz DOT grafiği граф DOT Graphviz Biểu đồ DOT Graphviz Graphviz DOT 科学图形 Graphviz DOT 圖 JAD document مستند JAD Dakument JAD Документ — JAD document JAD dokument JAD JAD-dokument JAD-Dokument Έγγραφο JAD JAD document JAD-dokumento documento JAD JAD dokumentua JAD-asiakirja JAD skjal document JAD cáipéis JAD documento JAD מסמך JAD JAD dokument JAD dokumentum Documento JAD Dokumen JAD Documento JAD JAD ドキュメント JAD құжаты JAD 문서 JAD dokumentas JAD dokuments JAD-dokument JAD-document JAD-dokument document JAD Dokument JAD documento JAD Documento JAD Document JAD документ JAD Dokument JAD Dokument JAD Dokument JAD ЈАД документ JAD-dokument JAD belgesi документ JAD Tài liệu JAD JAD 文档 JAD 文件 JAD Java Application Descriptor WML document مستند WML WML sənədi Dakument WML Документ — WML document WML dokument WML Dogfen WML WML-dokument WML-Dokument Έγγραφο WML WML document WML-dokumento documento WML WML dokumentua WML-asiakirja WML skjal document WML cáipéis WML documento WML מסמך WML WML dokument WML-dokumentum Documento WML Dokumen WML Documento WML WML ドキュメント WML құжаты WML 문서 WML dokumentas WML dokuments Dokumen XML WML-dokument WML-document WML-dokument document WML Dokument WML documento WML Documento WML Document WML документ WML Dokument WML Dokument WML Dokument WML ВМЛ документ WML-dokument WML belgesi документ WML Tài liệu WML WML 文档 WML 文件 WML Wireless Markup Language WMLScript program برنامج WMLScript Prahrama WMLScript Програма — WMLScript programa WMLScript program WMLScript WMLScript-program WMLScript-Programm Πρόγραμμα WMLScript WMLScript program programa en WMLScript WMLScript programa WMLScript-ohjelma WMLScript forrit programme WMLScript ríomhchlár WMLScript programa en WMLScript תכנית של WMLScript WMLScript program WMLScript program Programma WMLScript Program WMLScript Programma WMLScript WMLScript プログラム WMLScript бағдарламасы WMLScript 프로그램 WMLScript programa WMLScript programma WMLScript-program WMLScript-programma WMLScript-program programa WMLEscript Pogram WMLScript programa WMLScript Programa WMLScript Program WMLScript сценарий WMLScript Program WMLScript Programska datoteka WMLScript Program WMLScript програм ВМЛ скрипте WMLScript-program WMLScript programı програма мовою WMLScript Chương trình WMLScript WMLScript 程序 WMLScript 程式 ACE archive أرشيف ACE Archiŭ ACE Архив — ACE arxiu ACE archiv ACE ACE-arkiv ACE-Archiv Συμπιεσμένο αρχείο ACE ACE archive ACE-arkivo archivador ACE ACE artxiboa ACE-arkisto ACE skjalasavn archive ACE cartlann ACE arquivo ACE ארכיון ACE ACE arhiva ACE archívum Archivo ACE Arsip ACE Archivio ACE ACE アーカイブ ACE არქივი ACE архиві ACE 압축 파일 ACE archyvas ACE arhīvs ACE-arkiv ACE-archief ACE-arkiv archiu ACE Archiwum ACE arquivo ACE Pacote ACE Arhivă ACE архив ACE Archív ACE Datoteka arhiva ACE Arkiv ACE АЦЕ архива ACE-arkiv ACE arşivi архів ACE Kho nén ACE ACE 归档文件 ACE 封存檔 Ada source code شفرة مصدر Ada Kryničny kod Ada Изходен код — Ada codi font en Ada zdrojový kód v jazyce Ada Ada-kildekode Ada-Quelltext Πηγαίος κώδικας Ada Ada source code Ada-fontkodo código fuente en Ada Ada iturburu-kodea Ada-lähdekoodi Ada keldukota code source Ada cód foinseach Ada código fonte en Ada קוד מקור Ada Ada izvorni kod Ada-forráskód Codice-fonte Ada Kode program Ada Codice sorgente Ada Ada ソースコード Ada-ის საწყისი კოდი Ada бастапқы коды Ada 소스 코드 Ada pradinis kodas Ada pirmkods Kod sumber Ada Ada-kildekode Ada-broncode Ada-kjeldekode còde font Ada Kod źródłowy Ada código origem Ada Código-fonte Ada Cod sursă Ada исходный код Ada Zdrojový kód jazyka Ada Datoteka izvorne kode Ada Kod burues Ada Ада изворни ко̂д Ada-källkod Ada kaynak kodu вихідний код мовою Ada Mã nguồn Ada Ada 源代码 Ada 源碼 author list لائحة المؤلف śpis aŭtaraŭ Списък на авторите llista d'autors seznam autorů forfatterliste Autorenliste Κατάλογος συγγραφέων author list listo de aŭtoroj lista de autores egile-zerrenda tekijäluettelo høvundalisti liste d'auteurs liosta údar lista de autores רשימת יוצרים Popis autora szerzőlista Lista de autores senarai penulis Elenco autori 著者リスト авторлар тізімі 저자 목록 autorių sąrašas autoru saraksts Senarai penulis forfatterliste auteurslijst forfattarliste lista d'autors Lista autorów lista de autores Lista de autores listă autori список авторов Zoznam autorov seznam avtorjev Lista e autorëve списак аутора författarlista yazar listesi перелік авторів danh sách tác giả 作者列表 作者清單 BibTeX document مستند BibTeX Dakument BibTeX Документ — BibTeX document BibTeX dokument BibTeX BibTeX-dokument BibTeX-Dokument Έγγραφο BibTeX BibTeX document BibTeX-dokumento documento BibTeX BibTeX dokumentua BibTeX-asiakirja BibTeX skjal document BibTeX cáipéis BibTeX documento BibTex מסמך BibTeX BibTeX dokument BibTeX dokumentum Documento BibTeX Dokumen BibTeX Documento BibTeX BibTeX ドキュメント BibTeX-ის დოკუმენტი BibTeX құжаты BibTeX 문서 BibTeX dokumentas BibTeX dokuments BibTeX-dokument BibTeX-document BibTeX-dokument document BibTeX Dokument BibTeX documento BibTeX Documento BibTeX Document BibTeX документ BibTeX Dokument BibTeX Dokument BibTeX Dokument BibTeX Биб ТеКс документ BibTeX-dokument BibTeX belgesi документ BibTeX Tài liệu BibTeX BibTeX 文档 BibTeX 文件 C++ header ترويسة سي++ Zahałoŭny fajł C++ Заглавен файл — C++ capçalera en C++ hlavičkový soubor C++ C++-posthoved C++-Header Κεφαλίδα C++ C++ header cabecera de código fuente en C++ C++ goiburua C++-otsake C++ tekshøvd en-tête C++ ceanntásc C++ cabeceira de código fonte en C++ כותר C++‎ C++ zaglavlje C++ fejléc Capite C++ Tajuk C++ Header C++ C++ ヘッダー C++-ის თავსართი C++ тақырыптама файлы C++ 헤더 C++ antraštė C++ galvene C++-kildekodeheader C++-header C++-kjeldekode-hovud entèsta C++ Plik nagłówkowy C++ cabeçalho C++ Cabeçalho C++ Antet C++ заголовочный файл C++ Hlavičky jazyka C++ Datoteka glave C++ Header C++ Ц++ заглавље C++-huvud C++ başlığı файл заголовків мовою C++ Phần đầu mã nguồn C++ C++ 源代码头文件 C++ 標頭檔 C++ source code شفرة مصدر سي++ Kryničny kod C++ Изходен код — C++ codi font en C++ zdrojový kód C++ C++-kildekode C++-Quelltext Πηγαίος κώδικας C++ C++ source code C++-fontkodo código fuente en C++ C++ iturburu-kodea C++-lähdekoodi C++ keldukota code source C++ cód foinseach C++ código fonte de C++ קוד מקור של C++‎ C++ izvorni kod C++-forráskód Codice-fonte C++ Kode program C++ Codice sorgente C++ C++ ソースコード C++-ის საწყისი კოდი C++ бастапқы коды C++ 소스 코드 C++ pradinis kodas C++ pirmkods Kod sumber C++ C++-kildekode C++-broncode C++-kjeldekode còde font C++ Kod źródłowy C++ código origem C++ Código-fonte C++ Cod sursă C++ исходный код C++ Zdrojový kód jazyka C++ Datoteka izvorne kode C++ Kod burues C++ Ц++ изворни ко̂д C++-källkod C++ kaynak kodu вихідний код мовою C++ Mã nguồn C++ C++ 源代码 C++ 源碼 ChangeLog document مستند ChangeLog Dakument zafiksavanych źmienaŭ ChangeLog Дневник за промени — ChangeLog document de registre de canvis dokument ChangeLog ChangeLot-dokument Änderungsprotokoll-Dokument Έγγραφο ChangeLog ChangeLog document documento de registro de cambios ChangeLog dokumentua Muutoslokiasiakirja ChangeLog skjal document ChangeLog cáipéis ChangeLog documento Changelog מסמך של ChangeLog Dokument zaspisa promjena ChangeLog dokumentum Lista de cambiamentos Dokumen ChangeLog Documento ChangeLog ChangeLog ドキュメント ChangeLog დოკუმენტი ChangeLog құжаты ChangeLog 문서 ChangeLog dokumentas ChangeLog dokuments ChangeLog-dokument ChangeLog-document ChangeLog-dokument document ChangeLog Dokument zmian (ChangeLog) documento ChangeLog Documento ChangeLog Document ChangeLog протокол изменений Dokument ChangeLog Dokument ChangeLog Dokument ChangeLog Ченџ Лог документ Ändringsloggsdokument Değişim Günlüğü belgesi документ ChangeLog Tài liệu ChangeLog (ghi lưu thay đổi) 变更日志文档 ChangeLog 文件 C header ترويسة C Zahałoŭny fajł C Заглавен файл — C capçalera en C hlavičkový soubor C C-posthoved C-Header Κεφαλίδα C C header cabecera de código fuente en C C goiburua C-otsake C tekshøvd en-tête C ceanntásc C cabeceira de códifo fonte de C כותר C C zaglavlje C fejléc Capite C Tajuk C Header C C ヘッダー C-ის თავსართი C тақырыптама файлы C 헤더 C antraštė C galvene C-kildekodeheader C-header C-kjeldekode-hovud entèsta C Plik nagłówkowy C cabeçalho C Cabeçalho C Antet C заголовочный файл C Hlavičky jazyka C Datoteka glave C Header C Ц заглавље C-huvud C başlığı файл заголовків мовою C Phần đầu mã nguồn C C 程序头文件 C 標頭檔 CMake source code شفرة مصدر CMake Kryničny kod CMake Изходен код — CMake codi font en CMake zdrojový kód CMake CMake-kildekode CMake-Quelltext Πηγαίος κώδικας CMake CMake source code CMake-fontkodo código fuente en CMake CMake iturburu-kodea CMake-lähdekoodi CMake keldukota code source CMake cód foinseach CMake código fonte de CMake קוד מקור של CMake CMake izvorni kod CMake-forráskód Codice-fonte CMake Kode program CMake Codice sorgente CMake CMake ソースコード CMake-ის საწყისი კოდი CMake бастапқы коды CMake 소스 코드 CMake pirminis tekstas CMake pirmkods CMake-kildekode CMake-broncode CMake-kjeldekode còde font CMake Kod źródłowy CMake código origem CMake Código-fonte CMake Cod sursă CMake исходный код CMake Zdrojový kód CMake Datoteka izvorne kode CMake Kod burues CMake Ц Мејк изворни ко̂д CMake-källkod CMake kaynak kodu вихідний код CMake Mã nguồn CMake CMake 源代码 CMake 源碼 CSV document مستند CSV Dakument CSV Документ — CSV document CSV dokument CSV CSV-dokument CSV-Dokument Έγγραφο CSV CSV document CSV-dokumento documento CSV CSV dokumentua CSV-asiakirja CSV skjal document CSV cáipéis CSV documento CSV מסמך CSV CSV dokument CSV dokumentum Documento CSV Dokumen CSV Documento CSV CSV ドキュメント CSV დოკუმენტი CSV құжаты CSV 문서 CSV dokumentas CSV dokuments CSV-dokument CSV-document CSV-dokument document CSV Dokument CSV documento CSV Documento CSV Document CSV документ CSV Dokument CSV Dokument CSV Dokument CSV ЦСВ документ CSV-dokument CSV belgesi документ CSV Tài liệu CSV CSV 文档 CSV 文件 CSV Comma Separated Values CSV Schema document document Schema de CSV dokument schématu CSV CSV Schema-dokument CSV-Schemadokument CSV Schema document documento esquemático CSV CSV Schema dokumentua CSV Shema dokument CSV sémadokumentum Documento CSV Schema Dokumen Skema CSV Documento schema CSV CSV сұлба құжаты CSV 스키마 문서 Dokument schematu CSV documento CSV Schema Documento CSV Schema документ CSV Schema Dokument schémy CSV документ ЦСВ шеме CSV Schema-dokument CSV Şeması belgesi документ Schema у форматі CSV CSV 架构文档 CSV Schema 文件 CSV Comma Separated Values license terms شروط الترخيص licenzijnyja ŭmovy Лицензни условия condicions de llicència licenční podmínky licensbetingelser Lizenzbedingungen Όροι άδειας licence terms términos de licencia lizentzia baldintzak lisenssiehdot loyvistreytir termes de licence téarmaí ceadúnais termos de licenza תנאי רישיון uvjeti licence licencfeltételek Conditiones de licentia persyaratan lisensi Termini di licenza ソフトウェアライセンス条項 лицензиялық келісімі 라이선스 조항 licencijos sąlygos licences nosacījumi lisensbestemmelser licentievoorwaarden lisensvilkår tèrmes de licéncia Warunki licencji termos de licença Termos de licença termeni de licență лицензионное соглашение Licenčné podmienky pogoji in dovoljenja uporabe Kushte liçence услови коришћења licensvillkor lisans koşulları ліцензійні умови điều kiện giấy phép 软件许可条款 授權條款 author credits شكر وتقدير المؤلف zasłuhi aŭtara Благодарности към авторите atribucions d'autor autorské zásluhy bidragydere Autorendanksagung Μνεία συγγραφέων author credits reconocimiento de autoría tekijöiden kiitokset høvundaheiður remerciements admhálacha údar créditos de autor קרדיטים של היוצר Zasluge autora szerzők listája Recognoscentia de autores kredit penulis Riconoscimenti autori ソフトウェア作者クレジット бағдарлама авторлары 작성자 정보 padėkos autoriams veidotāji liste med bidragsytere auteursinformatie forfattarliste mercejaments Podziękowania autorów programu créditos de autor Créditos do autor mulțumiri autori авторы программы Autorské zásluhy avtorske zasluge Kreditë e autorëve заслуге аутора författarlista yazar bilgileri подяки авторам програми công trạng tác giả 软件作者致谢 作者致謝名單 C source code شفرة مصدر سي Kryničny kod C Изходен код — C codi font en C zdrojový kód C C-kildekode C-Quelltext Πηγαίος κώδικας C C source code C-fontkodo código fuente en C C iturburu-kodea C-lähdekoodi C keldukota code source C cód foinseach C código fonte en C קוד מקור של C C izvorni kod C-forráskód Codice-fonte C Kode program C Codice sorgente C C ソースコード C-ის საწყისი კოდი C бастапқы коды C 소스 코드 C pradinis kodas C pirmkods Kod sumber C C-kildekode C-broncode C-kjeldekode còde font C Kod źródłowy C código origem C Código-fonte C Cod sursă C исходный код C Zdrojový kód jazyka C Datoteka izvorne kode C Kod burues C Ц изворни ко̂д C-källkod C kaynak kodu вихідний код мовою C Mã nguồn C C 源代码 C 源碼 C# source code شفرة مصدر سي# Kryničny kod C# Изходен код — C# codi font en C# zdrojový kód C# C#-kildekode C#-Quelltext Πηγαίος κώδικας C# C# source code C#-fontkodo código fuente en C# C# iturburu-kodea C#-lähdekoodi C# keldukota code source C# cód foinseach C# código fonte en C# קוד מקור של C#‎ C# izvorni kod C#-forráskód Codice-fonte C# Kode program C# Codice sorgente C# C# ソースコード C#-ის საწყისი კოდი C# бастапқы коды C# 소스 코드 C# pradinis kodas C# pirmkods Kod sumber C# C#-kildekode C#-broncode C#-kjeldekode còde font C# Kod źródłowy C# código origem C# Código-fonte C# Cod sursă C# исходный код C# Zdrojový kód jazyka C# Datoteka izvorne kode C# Kod burues C# Ц# изворни ко̂д C#-källkod C# kaynak kodu вихідний код мовою C# Mã nguồn C# C# 源代码 C# 源碼 Vala source code شفرة مصدر Vala Kryničny kod Vala Изходен код — Vala codi font en Vala zdrojový kód Vala Valakildekode Vala-Quelltext Πηγαίος κώδικας Vala Vala source code Vala-fontkodo código fuente en Vala Vala iturburu-kodea Vala-lähdekoodi Vala keldukota code source Vala cód foinseach Vala código fonte en Vala קוד מקור של Vala Vala izvorni kod Vala forráskód Codice-fonte Vala Kode program Vala Codice sorgente Vala Vala ソースコード Vala бастапқы коды Vala 소스 코드 Vala pradinis kodas Vala pirmkods Vala-kildekode Vala-broncode Vala-kjeldekode còde font Vala Kod źródłowy Vala código origem Vala Código-fonte Vala Cod sursă Vala исходный код Vala Zdrojový kód Vala Datoteka izvorne kode Vala Kod burues Vala Вала изворни ко̂д Vala-källkod Vala kaynak kodu вихідний код мовою Vala Mã nguồn Vala Vala 源代码 Vala 源碼 OOC source code Изходен код — OOC codi font en OOC zdrojový kód OOC OOC-kildekode OOC-Quelltext Πηγαίος κώδικας OOC OOC source code OOC-fontkodo código fuente en OOC OOC iturburu-kodea OOC-lähdekoodi source code OOC código fonte de OOC קוד מקור של OOC OOC izvorni kod OOC forráskód Codice-fonte OCC Kode sumber OOC Codice sorgente OOC OOC ソースコード OOC-ის საწყისი კოდი OOC бастапқы коды OOC 소스 코드 OOC pirmkods OOC broncode font còde OOC Kod źródłowy OOC código origem OOC Código-fonte OOC исходный код OOC Zdrojový kód OOC Izvorna koda OOC ООЦ изворни ко̂д OOC-källkod OOC kaynak kodu вихідний код мовою OOC OOC OOC 源碼 OOC Out Of Class DCL script سكربت DCL DCL skripti Skrypt DCL Скрипт — DCL script DCL skript DCL Sgript DCL DCL-program DCL-Skript Δέσμη ενεργειών DCL DCL script DCL-skripto secuencia de órdenes en DCL DCL script-a DCL-komentotiedosto DCL boðrøð script DCL script DCL script de DCL תסריט DCL DCL skripta DCL-parancsfájl Script DCL Skrip DCL Script DCL DCL スクリプト DCL სცენარი DCL сценарийі DCL 스크립트 DCL scenarijus DCL skripts Skrip DCL DCL-skript DCL-script DCL-skript escript DCL Skrypt DCL script DCL Script DCL Script DCL сценарий DCL Skript DCL Skriptna datoteka DCL Script DCL ДЦЛ скрипта DCL-skript DCL betiği скрипт DCL Văn lệnh DCL DCL 脚本 DCL 指令稿 DCL Data Conversion Laboratory DSSSL document مستند DSSSL DSSSL sənədi Dakument DSSSL Документ — DSSSL document DSSSL dokument DSSSL Dogfen DSSSL DSSSL-dokument DSSSL-Dokument Έγγραφο DSSSL DSSSL document DSSSL-dokumento documento DSSSL DSSSL dokumentua DSSSL-asiakirja DSSSL skjal document DSSSL cáipéis DSSSL documento DSSSL מסמך DSSSL DSSSL dokument DSSSL-dokumentum Documento DSSSL Dokumen DSSSL Documento DSSSL DSSSL ドキュメント DSSSL დოკუმენტი DSSSL құжаты DSSSL 문서 DSSSL dokumentas DSSSL dokuments Dokumen DSSSL DSSSL-dokument DSSSL-document DSSSL-dokument document DSSSL Dokument DSSSL documento DSSSL Documento DSSSL Document DSSSL документ DSSSL Dokument DSSSL Dokument DSSSL Dokument DSSSL ДСССЛ документ DSSSL-dokument DSSSL belgesi документ DSSSL Tài liệu DSSSL DSSSL 文档 DSSSL 文件 DSSSL Document Style Semantics and Specification Language D source code شفرة مصدر D Kryničny kod D Изходен код — D codi font en D zdrojový kód D D-kildekode D-Quelltext Πηγαίος κώδικας D D source code D-fontkodo código fuente en D D iturburu-kodea D-lähdekoodi D keldukota code source D cód foinseach D código fonte de D קוד מקור לשפת D D izvorni kod D-forráskód Codice-fonte D Kode program D Codice sorgente D D ソースコード D-ის საწყისი კოდი D бастапқы коды D 소스 코드 D pradinis kodas D pirmkods D-kildekode D-broncode D-kjeldekode còde font D Kod źródłowy D código origem D Código-fonte D Cod sursă D исходный код D Zdrojový kód jazyka D Datoteka izvorne kode D Kod burues D Д изворни ко̂д D-källkod D kaynak kodu вихідний код мовою D Mã nguồn D D 源代码 D 源碼 DTD file ملف DTD Fajł DTD Документ — DTD fitxer DTD soubor DTD DTD-fil DTD-Datei Αρχείο DTD DTD file DTD-dosiero archivo DTD DTD fitxategia DTD-tiedosto DTD fíla fichier DTD comhad DTD ficheiro DTD מסמך DTD DTD datoteka DTD fájl File DTD Berkas DTD File DTD DTD ファイル DTD ფაილი DTD файлы DTD 파일 DTD failas DTD datne DTD-fil DTD-bestand DTD-fil fichièr DTD Plik DTD ficheiro DTD Arquivo DTD Fișier DTD файл DTD Súbor DTD Datoteka DTD File DTD ДТД датотека DTD-fil DTD dosyası файл DTD Tập tin DTD DTD 文件 DTD 檔 DTD Document Type Definition Eiffel source code شفرة مصدر Eiffel Kryničny kod Eiffel Изходен код — Eiffel codi font en Eiffel zdrojový kód Eiffel Eiffelkildekode Eiffel-Quelltext Πηγαίος κώδικας Eiffel Eiffel source code Eiffel-fontkodo código fuente en Eiffel Eiffel iturburu-kodea Eiffel-lähdekoodi Eiffel keldukota code source Eiffel cód foinseach Eiffel código fone de Eiffel קוד מקור של Eiffel Eiffel izvorni kod Eiffel forráskód Codice-fonte Eiffel Kode program Eiffel Codice sorgente Eiffel Eiffel ソースコード Eiffel-ის საწყისი კოდი Eiffel бастапқы коды Eiffel 소스 코드 Eiffel pirminis programos tekstas Eiffel pirmkods Eiffel-kildekode Eiffel-broncode Eiffel-kjeldekode còde font Eiffel Kod źródłowy Eiffel código origem Eiffel Código-fonte Eiffel Cod sursă Eiffel исходный код Eiffel Zdrojový kód Eiffel Datoteka izvorne kode Eiffel Kod burues Eiffel Ајфел изворни ко̂д Eiffel-källkod Eiffel kaynak kodu вихідний код мовою Eiffel Mã nguồn Eiffel Eiffel 源代码 Eiffel 源碼 Emacs Lisp source code شفرة مصدر Emacs Lisp Emacs Lisp mənbə kodu Kryničny kod Emacs Lisp Изходен код — Emacs Lisp codi font en Emacs Lisp zdrojový kód Emacs Lisp Ffynhonnell rhaglen EMACS LISP Emacs Lisp-kildekode Emacs-Lisp-Quelltext Πηγαίος κώδικας Emacs Lisp Emacs Lisp source code fontkodo en Emacs Lisp código fuente en Lisp de Emacs Emacs Lisp iturburu-kodea Emacs Lisp -lähdekoodi Emacs Lisp keldukota code source Emacs Lisp cód foinseach Emacs Lisp código fonte de Emacs Lisp קוד מקור של Emcas Lisp Emacs Lisp izvorni kod Emacs Lisp-forráskód Codice-fonte Lisp de Emacs Kode sumber Emacs Lisp Codice sorgente Emacs Lisp Emacs Lisp ソースコード Emacs-ის Lisp საწყისი კოდი Emacs Lisp бастапқы коды Emacs Lisp 소스 코드 Emacs Lisp pradinis kodas Emacs Lisp pirmkods Kod sumber Emacs Lisp Emacs Lisp-kildekode Emacs Lisp-broncode Emacs Lisp kjeldekode còde font Emacs Lisp Plik źródłowy Emacs Lisp código origem Emacs Lisp Código-fonte Lisp do Emacs Cod sursă Emacs Lisp исходный код Emacs Lisp Zdrojový kód Emacs Lisp Datoteka izvorne kode Emacs Lisp Kod burues Emacs Lisp Емакс Лисп изворни ко̂д Emacs Lisp-källkod Emacs Lisp kaynak kodu вихідний код мовою Emacs Lisp Mã nguồn Lisp Emacs Emacs Lisp 源代码 Emacs Lisp 源碼 Erlang source code شفرة مصدر Erlang Kryničny kod Erlang Изходен код — Erlang codi font en Erlang zdrojový kód Erlang Erlangkildekode Erlang-Quelltext Πηγαίος κώδικας Erlang Erlang source code Erlang-fontkodo código fuente en Erlang Erlang iturburu-kodea Erlang-lähdekoodi Erlang keldukota code source Erlang cód foinseach Erlang código fonte de Erlang קוד מקור של Erlang Erlang izvorni kod Erlang forráskód Codice-fonte Erlang Kode program Erlang Codice sorgente Erlang Erlang ソースコード Erlang-ის საწყისი კოდი Erlang бастапқы коды Erlang 소스 코드 Erlang pradinis kodas Erlang pirmkods Erlang-kildekode Erlang-broncode Erlang-kjeldekode còde font Erlang Kod źródłowy Erlang código origem Erlang Código-fonte Erlang Cod sursă Erlang исходный код Erlang Zdrojový kód Erlang Datoteka izvorne kode Erlang Kod burues Erlang Ерланг изворни ко̂д Erlang-källkod Erlang kaynak kodu вихідний код мовою Erlang Mã nguồn Erlang Erlang 源代码 Erlang 源碼 Fortran source code شفرة مصدر Fortran Fortran mənbə kodu Kryničny kod Fortran Изходен код — Fortran codi font en Fortran zdrojový kód Fortran Ffynhonnell rhaglen FORTRAN Fortrankildekode Fortran-Quelltext Πηγαίος κώδικας Fortran Fortran source code Fotran-fontkodo código fuente en Fortran Fortran-en iturburu-kodea Fortran-lähdekoodi Fortran keldukota code source Fortran cód foinseach Fortran código fonte de Fortran קוד מקור של Fortran Fortran izvorni kod Fortran-forráskód Codice-fonte Fortran Kode program Fortran Codice sorgente Fortran Fortran ソースコード Fortran-ის საწყისი კოდი Fortran бастапқы коды 포트란 소스 코드 Fortran pradinis kodas Fortran pirmkods kod sumber Fortran Fortran-kildekode Fortran-broncode Fortran-kjeldekode còde font Fortran Kod źródłowy Fortran código origem Fortran Código-fonte Fortran Cod sursă Fortran исходный код Fortran Zdrojový kód Fortran Datoteka izvorne kode Fortran Kod burues Fortran Фортран изворни ко̂д Fortran-källkod Fortran kaynak kodu вихідний код мовою Fortran Mã nguồn Fortran Fortran 源代码 Fortran 源碼 Genie source code codi font de Genius zdrojový kód v jazyce Genie Genie-kildekode Genie-Quelltext Πηγαίος κώδικας Genie Genie source code código fuente en Genie Genie iturburu-kodea Genie-lähdekoodi Genie izvorni kôd Genie forráskód Codice-fonte Genie Kode program Genie Codice sorgente Genie Genie бастапқы коды Genie 소스 코드 còde font Genie Kod źródłowy Genie código origem Genie Código-fonte Genie исходный код Genie Zdrojový kód Genie Izvorna koda Genie Гение изворни ко̂д Genie-källkod Genie kaynak kodu вихідний код мовою Genie Genie 源代码 Genie 源碼 translation file ملف الترجمة fajł pierakładu Превод fitxer de traducció soubor překladu oversættelsesfil Übersetzungsdatei Αρχείο μετάφρασης translation file tradukad-dosiero archivo de traducción itzulpen-fitxategia käännöstiedosto týðingarfíla fichier de traduction comhad aistrithe ficheiro de tradución קובץ תרגום datoteka prijevoda fordítási fájl File de traduction berkas terjemahan File traduzione 翻訳ファイル თარგმნის ფაილი аудармалар файлы 번역 파일 vertimo failas tulkošanas datne oversettelsesfil vertalingsbestand omsetjingsfil fichièr de traduccion Plik tłumaczenia ficheiro de tradução Arquivo de tradução fișier traducere файл переводов Súbor prekladu datoteka prevoda programa File përkthimesh датотека превода översättningsfil çeviri dosyası файл перекладу tập tin dịch 消息翻译文件 翻譯檔 translation template قالب الترجمة šablon dla pierakładu Шаблон за преводи plantilla de traducció šablona překladu oversættelsesskabelon Übersetzungsvorlage Πρότυπο μετάφρασης translation template tradukad-ŝablono plantilla de traducción itzulpenen txantiloia käännösmalli týðingarformur modèle de traduction teimpléad aistrithe plantilla de tradución תבנית תרגום predložak prijevoda fordítási sablon Patrono de traduction templat terjemahan Modello di traduzione 翻訳テンプレート თარგმნის შაბლონი аудармалар үлгісі 메시지 번역 서식 vertimo šablonas tulkošanas veidne mal for oversetting vertalingssjabloon omsetjingsmal modèl de traduccion Szablon tłumaczenia modelo de tradução Modelo de tradução șablon de traducere шаблон переводов Šablóna prekladu predloga datoteke prevoda programa Model përkthimesh шаблон превода översättningsmall çeviri şablonu шаблон перекладу mẫu dịch 消息翻译模板 翻譯模版 feature specification in Gherkin format HTML document مستند HTML Dakument HTML Документ — HTML document HTML dokument HTML HTML-dokument HTML-Dokument Έγγραφο HTML HTML document HTML-dokumento documento HTML HTML dokumentua HTML-asiakirja HTML skjal document HTML cáipéis HTML documento HTML מסמך HTML HTML dokument HTML dokumentum Documento HTML Dokumen HTML Documento HTML HTML ドキュメント HTML құжаты HTML 문서 HTML dokumentas HTML dokuments HTML-dokument HTML-document HTML-dokument document HTML Dokument HTML documento HTML Documento HTML Document HTML документ HTML Dokument HTML Dokument HTML Dokument HTML ХТМЛ документ HTML-dokument HTML belgesi документ HTML Tài liệu HTML HTML 文档 HTML 文件 HTML HyperText Markup Language Web application cache manifest قائمة التخزين الموقت لتطبيق الويب Манифест за кеша на уеб приложение manifest de memòria cau d'aplicació Web manifest mezipaměti webové aplikace Manifest for internetprogrammellemlager Webanwendungscache-Manifest Δηλωτικό λανθάνουσας μνήμης εφαρμογής Ιστού Web application cache manifest manifiesto de caché de aplicación web Web aplikazioaren cache-aren agiria Net nýtsluskipanarkova manifest manifeste de cache d'application Web lastliosta taisce d'fheidhmchlár Gréasáin manifesto de caché de aplicativo web הצהרה של מטמון של תוכנית ברשת Web aplikacija prikaza predmemorije Webalkalmazás gyorsítótár-összefoglalója Manifesto de cache de application web Manifes singgahan aplikasi web Manifesto cache applicazione Web Web アプリケーションキャッシュ manifest Веб қолданбасының кэш манифесті 웹 애플리케이션 캐시 정의 Žiniatinklio programos podėlio manifestas Tīmekļa lietotņu keša manifests Webapplicatie cache manifest manifèste d'escondedor d'aplicacion Web Manifest pamięci podręcznej aplikacji WWW manifesto de cache de aplicação web Manifest de cache de aplicação web Manifest de cache pentru aplicații web манифест кэша веб-приложения Manifest vyrovnávacej pamäte webovej aplikácie Predpomnilnik spletnega programa проглас оставе Веб програма Cachemanifest för webbapplikation Web uygulama önbelleği bildirimi маніфест кешу веб-програми 网络应用程序缓存清单 網頁應用程式快取聲明 Google Video Pointer مؤشر فيديو جوجل Pakazalnik Google Video Документ-указател към видео на Google apuntador a vídeo de Google Google Video Pointer Google Video-peger Google Video-Zeiger Google Video Pointer Google Video Pointer lista de reproducción de Google Video (GVP) Google Video-ren erreprodukzio-zerrenda Google-video-osoitin Google Video Pointer pointeur vidéo Google pointeoir Google Video punteiro de vídeo de Google מצביע וידאו של Google Google Video pretraživač Google Video Pointer Punctator Google Video Google Video Pointer Puntatore Google Video Google ビデオポインター Google Video Pointer Google 동영상 포인터 Google Video Pointer Google Video Pointer Peker til Google Video Google-videoverwijzing Google Video-peikar puntador vidèo Google Lista odtwarzania Google Video Ponteiro Google Video Ponteiro do Google Vídeo Indicator Google Video Google Video Pointer Google Video Pointer Kazalec Google Video Puntues Google Video Гуглов видео показивач Google Video-pekare Google Video İşaretçisi вказівник відео Google Con trỏ ảnh động Google Google 视频指向 Google Video Pointer Haskell source code شفرة مصدر Haskell Haskell mənbə kodu Kryničny kod Haskell Изходен код на Haskell codi font en Haskell zdrojový kód Haskell Ffynhonnell rhaglen Haskell Haskellkildekode Haskell-Quelltext Πηγαίος κώδικας Haskell Haskell source code Haskell-fontkodo código fuente en Haskell Haskell iturburu-kodea Haskell-lähdekoodi Haskell keldukota code source Haskell cód foinseach Haskell código fonte de Haskell קוד מקור של Haskell Haskell izvorni kod Haskell-forráskód Codice-fonte Haskell Kode program Haskell Codice sorgente Haskell Haskell ソースコード Haskell бастапқы коды Haskell 소스 코드 Haskell pradinis kodas Haskell pirmkods Kod sumber Haskell Haskell-kildekode Haskell-broncode Haskell-kjeldekode còde font Haskell Kod źródłowy Haskell código origem Haskell Código-fonte Haskell Cod sursă Haskell исходный код Haskell Zdrojový kód Haskell Datoteka izvorne kode Haskell Kod burues Haskell Хаскел изворни ко̂д Haskell-källkod Haskell kaynak kodu вихідний код мовою Haskell Mã nguồn Haskell Haskell 源代码 Haskell 源碼 IDL document مستند IDL IDL sənədi Dakument IDL Документ — IDL document IDL dokument IDL Dogfen IDL IDL-dokument IDL-Dokument Έγγραφο IDL IDL document IDL-dokumento documento IDL IDL dokumentua IDL-asiakirja IDL skjal document IDL cáipéis IDL documento IDL מסמך IDL IDL dokument IDL-dokumentum Documento IDL Dokumen IDL Documento IDL IDL ドキュメント IDL құжаты IDL 문서 IDL dokumentas IDL dokuments Dokumen IDL IDL-dokument IDL-document IDL-dokument document IDL Dokument IDL documento IDL Documento IDL Document IDL документ IDL Dokument IDL Dokument IDL Dokument IDL ИДЛ документ IDL-dokument IDL belgesi документ IDL Tài liệu IDL IDL 文档 IDL 文件 IDL Interface Definition Language installation instructions تعليمات التثبيت instrukcyja dla instalavańnia Инструкции за инсталация instruccions d'instal·lació návod k instalaci installationsinstruktioner Installationsanleitung Οδηγίες εγκατάστασης installation instructions instrucciones de instalación instalazioaren instrukzioak asennusohjeet innleggingar vegleiðing instructions d'installation treoracha suiteála instrucións de instalación הוראות התקנה upute za instalaciju telepítési utasítások Instructiones de installation instruksi instalasi Istruzioni di installazione ソフトウェアインストール説明 бағдарламаны орнату нұсқаулары 설치 방법 diegimo instrukcijos instalācijas instrukcijas installationsinstruksjoner installatie-instructies installasjonsinstruksjonar instructions d'installacion Instrukcje instalacji instruções de instalação Instruções de instalação instrucțiuni de instalare инструкции по установке программы Návod na inštaláciu navodila namestitve Udhëzime instalimi упутства инсталације installationsinstruktioner kurulum yönergeleri інструкції з встановлення hướng dẫn cài đặt 软件安装指南 安裝指引 Java source code شفرة مصدر Java Kryničny kod Java Изходен код на Java codi font en Java zdrojový kód Java Javakildekode Java-Quelltext Πηγαίος κώδικας Java Java source code Java-fontkodo código fuente en Java Java iturburu-kodea Java-lähdekoodi Java keldukota code source Java cód foinseach Java código fonte de Java קוד מקור ב־Java Java izvorni kod Java-forráskód Codice-fonte Java Kode program Java Codice sorgente Java Java ソースコード Java бастапқы коды Java 소스 코드 Java pradinis kodas Java pirmkods Kod sumber Java Java-kildekode Java-broncode Java-kjeldekode còde font Java Kod źródłowy Java código origem Java Código-fonte Java Cod sursă Java исходный код Java Zdrojový kód Java Datoteka izvorne kode Java Kod burues Java Јава изворни ко̂д Java-källkod Java kaynak kodu вихідний код мовою Java Mã nguồn Java Java 源代码 Java 源碼 LDIF address book دفتر عناوين LDIF Adrasnaja kniha LDIF Адресна книга — LDIF llibreta d'adreces LDIF adresář LDIF LDIF-adressebog LDIF-Adressbuch Βιβλίο διευθύνσεων LDIF LDIF address book LDIF-adresaro libreta de direcciones LDIF LDIF helbide-liburua LDIF-osoitekirja LDIF adressubók carnet d'adresses LDIF leabhar sheoltaí LDIF lista de enderezos LDIF ספר כתובות של LDIF LDIF adresar LDIF címjegyzék Adressario LDIF Buku alamat LDIF Rubrica LDIF LDIF アドレス帳 LDIF адрестер кітабы LDIF 주소록 LDIF adresų knygelė LDIF adrešu grāmata LDIF-adressebok LDIF-adresboek LDIF-adressebok quasernet d'adreças LDIF Książka adresowa LDIF livro de endereços LDIF Livro de endereços LDIF Agendă LDIF адресная книга LDIF Adresár LDIF Datoteka imenika naslovov LDIF Rubrikë LDIF ЛДИФ адресар LDIF-adressbok LDIF adres defteri адресна книга LDIF Sổ địa chỉ LDIF LDIF 地址簿 LDIF 通訊錄 LDIF LDAP Data Interchange Format Lilypond music sheet صفحة موسيقى Lilypond Muzyčny arkuš Lilypond Нотация на Lilypond full de música Lilypond notový papír Lilypond Lilypondmusikkort Lilypond-Notenblatt Παρτιτούρα Lilypond Lilypond music sheet partitura de LilyPond Lilypond musika-orria Lilypond-nuotit Lilypond tónleika ark partition musicale Lilypond bileog cheoil Lilypond folla de música de Lilypond דף מוזיקה של Lilypond Lilypond glazbena ljestvica Lilypond kotta Partition musical Lilypond Lembar musik Lilypond Partitura Lilypond Lilypond 楽譜データ Lilypond музыка тізімі Lilypond 악보 Lilypond muzikos lapas Lilypond mūzikas lapa Lilypond-muziekblad Lilypond noteark particion musicala Lilypond Plik partytury Lilypond folha de música Lilypond Partitura do Lilypond Fișă muzică Lilypond список музыки Lilypond Notový papier Lilypond Glasbena predloga Lilypond Partiturë Lilypond Лилипонд музички лист Lilypond-notblad Lilypond müzik sayfası нотний запис Lilypond Bản nhạc Lilypond Lilypond 乐谱 Lilypond 樂譜 LHS source code شفرة مصدر LHS Kryničny kod LHS Изходен код на LHS codi font en LHS zdrojový kód LHS LHS-kildekode LHS-Quelltext Πηγαίος κώδικας LHS LHS source code LHS-fontkodo código fuente en LHS LHS iturburu-kodea LHS-lähdekoodi LHS keld code source LHS cód foinseach LHS código fonte en LHS קוד מקור של LHS LHS izvorni kod LHS forráskód Codice-fonte LHS Kode program LHS Codice sorgente LHS LHS ソースコード LHS бастапқы коды LHS 소스 코드 LHS pradinis kodas LHS pirmkods LHS-kildekode LHS-broncode LHS-kjeldekode còde font LHS Kod źródłowy LHS código origem LHS Código-fonte LHS Cod sursă LHS исходный код LHS Zdrojový kód LHS Datoteka izvorne kode LHS Kod burues LHS ЛХС изворни ко̂д LHS-källkod LHS kaynak kodu вихідний код LHS Mã nguồn LHS LHS 源代码 LHS 源碼 LHS Literate Haskell source code application log سجل التطبيق časopis aplikacyi Файл-дневник на приложение registre d'aplicació záznam aplikace programlog Anwendungsprotokoll Καταγραφή εφαρμογή application log protokolo de aplikaĵo registro de aplicación aplikazio egunkaria sovelluksen lokitiedosto nýtsluskipan logg journal d'application logchomhad feidhmchláir rexistro de aplicativo יומן יישום Zapis aplikacije alkalmazás naplója Registro de application log aplikasi Registro applicazione アプリケーションログ мәлімдемелер журналы 프로그램 기록 programos žurnalas lietotnes žurnāls Log aplikasi applikasjonslogg programma-logbestand programlogg jornal d'aplicacion Dziennik programu diário de aplicação Registro de aplicativo înregistrare aplicație журнал сообщений Záznam aplikácie dnevnik programa log i mesazheve të programit дневник програма programlogg uygulama günlüğü журнал програми bản ghi ứng dụng 应用程序日志 程式紀錄檔 Makefile ملف Makefile İnşa faylı Makefile Файл — make Makefile Makefile Ffeil "make" Bygningsfil Makefile Makefile Makefile Muntodosiero Makefile Makefile Makefile Makefile makefile Makefile Makefile Makefile Makefile Makefile Makefile Makefile Makefile Makefile Makefile Makefile (жинау файлы) Makefile Makefile Makefile Makefile Makefile Makefile Makefile makefile Plik make Makefile Makefile (arquivo do make) Makefile Makefile (файл сборки) Makefile Datoteka Makefile Makefile датотека стварања Makefil Makefile файл проекту make Tập tin tạo ứng dụng (Makefile) Makefile Makefile Markdown document Документ — Markdown document Markdown dokument Markdown Markdown-dokument Markdown-Dokument Έγγραφο Markdown Markdown document documento Markdown Markdown dokumentua Markdown-asiakirja document Markdown documento de Markdown מסמך Markdown Markdown dokument Markdown dokumentum Documento Markdown Dokumen markdown Documento Markdown Markdown Markdown құжаты 마크다운 문서 Markdown dokuments Markdown document document Markdown Dokument Markdown documento Markdown Documento Markdown документ Markdown Dokument Markdown Dokument Markdown Маркдаун документ Markdown-dokument Markdown belgesi документ Markdown Markdown 文档 Markdown 文件 Qt MOC file ملف Qt MOC Fajł Qt MOC Файл — Qt MOC fitxer MOC de Qt soubor Qt MOC Qt MOC-fil Qt-MOC-Datei Αρχείο Qt MOC Qt MOC file archivo MOC Qt Qt MOC fitxategia Qt MOC -tiedosto Qt MOC fíla fichier Qt MOC comhad MOC Qt ficheiro MOC Qt קובץ Qt MOC Qt MOC datoteka Qt MOC fájl File Qt MOC Berkas Qt MOC File MOC Qt Qt MOC ファイル Qt MOC файлы Qt MOC 파일 Qt MOC failas Qt MOC datne Qt MOC-fil Qt MOC-bestand Qt MOC-fil fichièr Qt MOC Plik Qt MOC ficheiro Qt MOC Arquivo Qt MOC Fișier Qt MOC файл Qt MOC Súbor Qt MOC Datoteka Qt MOC File Qt MOC Кут МОЦ датотека Qt MOC-fil Qt MOC dosyası файл-метаоб'єкт Qt Tập tin MOC của Qt Qt 元对象编译文件 Qt MOC 檔 Qt MOC Qt Meta Object Compiler Windows Registry extract استخراج مسجل ويندوز Element rehistru Windows Извадка от регистъра на Windows extracte de Windows Registry výtah registru Windows Windows Registy-udtrækning Windows-Registry-Auszug Αποσυμπίεση Windows Registry Windows Registry extract extracto del registro de Windows Windows-eko erregistro erauzlea Windows-rekisteritietue Windows Registry úrdráttur extrait de registre Windows sliocht as Clárlann Windows Extracto do rexistro de Windows קובץ רשומות מערכת של Windows Izdvojeni Windows registar Windows Registry kivonat Extracto de registro de systema Windows Ekstrak Windows Registry Estratto Windows Registry WIndows レジストリ抽出ファイル Windows Registry бөлігі Windows 레지스트리 파일 Windows registro ištrauka Windows Registry izvilkums Utdrag av Windows Registry Windows Registry-extract Windows Registry-utdrag extrait de registre Windows Wycinek rejestru Windows extrato do registo do Windows Extrator de registro do Windows Extras al registrului Windows фрагмент Windows Registry Časť registrov Windows izvleček vpisnika Windows Pjesë Windows Registry исцедак Виндоузовог регистра Windows Registry-utdrag Windows Kayıt Defteri özü частина реєстру Windows Bản trích Registry Windows Windows 注册表文件 Windows Registry 抽出 Managed Object Format صيغة كائن مدار Farmat Managed Object Управлявани обекти — MOF format d'objecte gestionat Managed Object Format Håndteret objektformat Managed Object Format Μορφή διαχειριζόμενου αντικειμένου Managed Object Format formato de objeto gestionado Kudeatutako objektu formatua Managed Object Format format Managed Object formáid réada bainistithe formato de obxecto xestionado תבנית פריט מנוהל Managed Object Format Felügyelt objektum (MO) formátum File in formato Managed Object Managed Object Format Managed Object Format 管理オブジェクトフォーマット Басқарылатын объект пішімі 관리되는 객체 형식 Sutvarkytų objektų formatas Pārvaldītu objektu formāts Managed Object Format Managed Object Format Managed Object Format format Managed Object Plik Managed Object Format formato Managed Object Formato de objeto gerenciado Managed Object Format формат управляемого объекта Formát Managed Object Datoteka Managed Object Managed Object Format запис управљаног објекта Managed Object Format Yönetilen Nesne Biçimi формат керування об’єктами Định dạng Đối tượng đã Quản lý 托管对象格式 Managed Object Format Mup publication منشور Mup Publikacyja Mup Издание — Mup publicació Mup publikace Mup Mupudgivelse Mup-Veröffentlichung Δημοσίευση Mup Mup publication publicación Mup Mup publikazioa Mup-julkaisu Mup útgáva publication Mup foilseachán Mup publicación Mup פרסום של Mup Mup publikacija Mup publikáció Publication Mup Publikasi Mup Pubblicazione Mup Mup 出版ファイル Mup жариялымы Mup 출판물 Mup leidinys Mup publikācija Mup publikasjon Mup-publicatie Mup-publikasjon publication Mup Publikacja Mup publicação Mup Publicação do Mup Publicație Mup публикация Mup Publikácie Mup Datoteka objave Mup Publikim Mup Муп објава Mup-publicering Mup uygulaması публікація Mup Bản xuất Mup Mup 应用程序 Mup 出版品 Objective-C source code شفرة مصدر الهدف-C Kryničny kod Objective-C Изходен код — Objective C codi font en Objective-C zdrojový kód Objective-C Objektiv C-kildekode Objective-C-Quelltext Πηγαίος κώδικας Objective-C Objective-C source code fontkodo en Objective-C código fuente en Objective-C Objective-C iturburu-kodea Objective-C-lähdekoodi Objective-C keldukota code source Objective-C cód foinseach Objective-C código fonte de Objective-C קוד מקור של Objective-C Objective-C izvorni kôd Objective-C forráskód Codice-fonte Objective-C Kode program Objective-C Codice sorgente Objective-C Objective-C ソースコード Objective-C-ის საწყისი კოდი Objective-C бастапқы коды Objective-C 소스 코드 Objective-C pradinis kodas Objective-C pirmkods Kod sumber Objective-C Objective-C-kildekode Objective-C-broncode Objective-C-kjeldekode còde font Objective-C Kod źródłowy Objective-C código origem Objective-C Código-fonte Objective-C Cod sursă Objective-C исходный код Objective-C Zdrojový kód Objective-C Datoteka izvorne kode Objective-C Kod burues C objekt Објектни-Ц изворни ко̂д Objective-C-källkod Objective-C kaynak kodu вихідний код мовою Objective-C Mã nguồn Objective-C Objective-C 源代码 Objective-C 源碼 OCaml source code شفرة مصدر OCaml Kryničny kod OCaml Изходен код — OCaml codi font en OCaml zdrojový kód OCaml OCaml-kildekode OCaml-Quelltext Πηγαίος κώδικας OCaml OCaml source code OCaml-fontkodo código fuente en OCaml OCaml iturburu-kodea OCaml-lähdekoodi OCaml keldukota code source OCaml cód foinseach OCaml código fonte de OCaml קוד מקור של OCaml OCaml izvorni kod OCaml forráskód Codice-fonte OCaml Kode program OCaml Codice sorgente OCaml OCaml ソースコード OCaml бастапқы коды OCaml 소스 코드 OCaml pradinis kodas OCaml pirmkods OCaml-kildekode OCaml-broncode OCaml-kjeldekode còde font OCaml Kod źródłowy OCaml código origem OCaml Código-fonte OCaml Cod sursă OCaml исходный код OCaml Zdrojový kód OCaml Datoteka izvorne kode OCaml Kod burues OCaml Окемл изворни ко̂д OCaml-källkod OCaml kaynak kodu первинний код мовою OCaml Mã nguồn OCaml OCaml 源代码 OCaml 源碼 MATLAB script/function سكربت/وظيفة MATLAB Skrypt/funkcyja MATLAB Скрипт/функция — MATLAB script/funció MATLAB skript/funkce MATLAB MATLAB-program/-funktion MATLAB-Skript/-Funktion Δέσμη ενεργειών/συνάρτηση MATLAB MATLAB script/function secuencia de órdenes/función de MATLAB MATLAB script/funtzioa MATLAB-komentotiedosto/funktio MATLAB boðrøð/funka script/fonction MATLAB script/feidhm MATLAB función/script de MATLAB תסריט/פונקציית MATLAB MATLAB skripta/funkcija MATLAB parancsfájl/funkció Script/function MATLAB Skrip/fungsi MATLAB Script/Funzione MATLAB MATLAB スクリプト/関数 MATLAB сценарий/функциясы MATLAB 스크립트/함수 MATLAB scenarijus / funkcija MATLAB skripts/funkcija Skript/funksjon for MATLAB MATLAB-script/functie MATLAB-skript/funksjon escript/fonction MATLAB Skrypt/funkcja MATLAB script/função MATLAB Script/função do MATLAB Funcție/script MATLAB сценарий/функция MATLAB Skript/funkcia MATLAB Skriptna datoteka MATLAB Script/Funksion MATLAB скрипта/функција МАТЛАБ-а MATLAB-skript/funktion MATLAB betiği/fonksiyonu скрипт/функція MATLAB Văn lệnh/chức năng MATLAB MATLAB 脚本/函数 MATLAB 指令稿/函式 Meson source code codi font en Meson zdrojový kód Meson Meson-kildekode Meson-Quelltext Πηγαίος κώδικας Meson Meson source code código fuente en Meson Meson iturburu-kodea Meson-lähdekoodi Meson izvorni kôd Meson forráskód Codice-fonte Meson Kode program Meson Codice sorgente Meson Meson бастапқы коды Meson 소스 코드 còde font Meson Kod źródłowy Meson código origem Meson Código-fonte Meson исходный код Meson Zdrojový kód Meson Месон изворни ко̂д Meson-källkod Meson kaynak kodu вихідний код мовою Meson Meson 源代码 Meson 源碼 Modelica model model de Modelica model Modelica Modelica-model Modelica-Modell Μοντέλο Modelica Modelica model modelo de Modelica Modelica modeloa Modelica-malli modèle Modelica Modelo de Modelica דגם של Modelica Modelica model Modelica modell Modello Modelica Model Modelica Modello Modelica Modelica モデル Modelica моделі Modelica 모델 Modelica modelis modèl Modelica Model Modelica modelo Modelica Modelo da Modelica модель Modelica Model Modelica Model Modelica модел Моделике Modelica-modell Modelica modeli модель Modelica Modelica 模型 Modelica 模型 Pascal source code شفرة مصدر باسكال Kryničny kod Pascal Изходен код — Pascal codi font en Pascal zdrojový kód Pascal Pascalkildekode Pascal-Quelltext Πηγαίος κώδικας Pascal Pascal source code Pascal-fontkodo código fuente en Pascal Pascal iturburu-kodea Pascal-lähdekoodi Pascal keldukota code source Pascal cód foinseach Pascal código fonte en Pascal קוד מקור של Pascal Pascal izvorni kôd Pascal-forráskód Codice-fonte Pascal Kode program Pascal Codice sorgente Pascal Pascal ソースコード Pascal бастапқы коды 파스칼 소스 코드 Pascal pradinis kodas Pascal pirmkods Kod sumber Pascal Pascal-kildekode Pascal-broncode Pascal-kjeldekode còde font Pascal Kod źródłowy Pascal código origem Pascal Código-fonte Pascal Cod sursă Pascal исходный код Pascal Zdrojový kód Pascal Datoteka izvorne kode Pascal Kod burues Pascal Паскалов изворни ко̂д Pascal-källkod Pascal kaynak kodu вихідний код мовою Pascal Mã nguồn Pascal Pascal 源代码 Pascal 源碼 differences between files الاختلافات بين الملفات adroźnieńni pamiž fajłami Разлики между файлове diferències entre fitxers rozdíly mezi soubory forskel mellem filer Unterschiede zwischen Dateien Διαφορές μεταξύ αρχείων differences between files diferencoj inter dosieroj diferencias entre archivos fitxategien arteko ezberdintasunak tiedostojen väliset erot munur millum fílur différences entre fichiers difríochtaí idir chomhaid diferenzas entre ficheiros הבדל בין קבצים razlike između datoteka diff-különbségfájl Differentias inter files perbedaan diantara berkas Differenze tra file ファイル間差分 файлдар арасындағы айырмашылықтары 파일 사이의 차이점 skirtumai tarp failų divu datņu atšķirība Perbezaan antara fail forskjeller mellom filer verschillen tussen bestanden skilnader mellom filer différences entre fichièrs Różnica pomiędzy plikami diferenças entre ficheiros Diferenças entre arquivos diferențe între fișiere различия между файлами Rozdiely medzi súbormi razlike med datotekami Diferencë midis file разлике између датотека skillnader mellan filer dosyalar arasındaki fark різниця між файлами khác biệt giữa các tập tin 文件的区别 檔案內容差異 Go source code Изходен код — Go codi font en Go zdrojový kód Go Go-kildekode Go-Quelltext Πηγαίος κώδικας Go Go source code Go-fontkodo código fuente en Go Go iturburu-kodea Go-lähdekoodi code source Go código fonte de Go קוד מקור של Go Go izvorni kod Go forráskód Codice-fonte Go Kode sumber Go Codice sorgente Go Go ソースコード Go-ის საწყისი კოდი Go бастапқы коды Go 소스 코드 Go pirmkods Go broncode còde font Go Kod źródłowy Go cigo origem Go Código-fonte Go исходный код Go Zdrojový kód Go Izvorna koda Go Гоу изворни ко̂д Go-källkod Go kaynak kodu вихідний код мовою Go Go Go 源碼 SCons configuration file fitxer de configuració de SCons konfigurační soubor SCons SCons-konfigurationsfil SCons-Konfigurationsdatei Αρχείο ρυθμίσεων SCons SCons configuration file archivo de configuración de SCons SCons konfigurazio-fitxategia SCons-asetustiedosto SCons datoteka podešavanja SCons beállítófájl File de cofniguration SCons Berkas konfigurasi SCons File configurazione SCons SCons баптаулар файлы SCons 설정 파일 fichièr de configuracion SCons Plik konfiguracji SCons ficheiro de configuração SCons Arquivo de configuração do SCons файл настроек SCons Konfiguračný súbor SCons Prilagoditvena datoteka SCons СКонс датотека подешавања SCons-konfigurationsfil SCons yapılandırma dosyası файл налаштувань SCons SCons 配置文件 SCons 組態檔 Python script سكربت بايثون Skrypt Python Скрипт — Python script Python skript Python Pythonprogram Python-Skript Δέσμη ενεργειών Python Python script Python-skripto secuencia de órdenes en Python Python script-a Python-komentotiedosto Python boðrøð script Python script Python Script en Python תסריט Python Python skripta Python-parancsfájl Script Python Skrip Python Script Python Python スクリプト Python сценарийі 파이썬 스크립트 Python scenarijus Python skripts Skrip Python Python-skript Python-script Python-skript escript Python Skrypt Python script Python Script Python Script Python сценарий Python Skript Python Skriptna datoteka Python Script Python Питонова скрипта Pythonskript Python betiği скрипт мовою Python Văn lệnh Python Python 脚本 Python 指令稿 Lua script سكربت Lua Skrypt Lua Скрипт на Lua script Lua skript Lua Luaprogram Lua-Skript Δέσμη ενεργειών Lua Lua script Lua-skripto secuencia de órdenes en Lua Lua script-a Lua-komentotiedosto Lua boðrøð script Lua script Lua script de Lua תסריט Lua Lua skripta Lua parancsfájl Script Lua Skrip Lua Script Lua Lua スクリプト Lua сценарийі Lua 스크립트 Lua scenarijus Lua skripts Lua-skript Lua-script Lua-skript escript Lua Skrypt Lua script Lua Script Lua Script Lua сценарий Lua Skript Lua Skriptna datoteka Lua Script Lua Луа скрипта Lua-skript Lua betiği скрипт Lua Văn lệnh Lua Lua 脚本 Lua 指令稿 README document مستند README README sənədi Dakument README Документ — „Да се прочете“ document README dokument README Dogfen README README-dokument README-Dokument Έγγραφο README README document README-dokumento documento README README dokumentua LUEMINUT-asiakirja README skjal document LISEZ-MOI cáipéis README documento README מסמך README README dokument README-dokumentum Documento LEGE-ME Dokumen README Documento README README ドキュメント README құжаты README 문서 README dokumentas README dokuments Dokumen README README-dokument LEESMIJ-document README-dokument document LISEZ-MOI Dokument README documento LEIA-ME Documento README Document README документ README Dokument README Dokument README Dokument README документ ПРОЧИТАЈМЕ README-dokument BENİOKU belgesi документ README Tài liệu Đọc Đi (README) README 文档 README 說明文件 NFO document مستند NFO Dakument NFO Документ — NFO document NFO dokument NFO NFO-dokument NFO-Dokument Έγγραφο NFO NFO document NFO-dokumento documento NFO NFO dokumentua NFO-asiakirja NFO skjal document NFO cáipéis NFO documento NFO מסמך NFO NFO dokument NFO-dokumentum Documento NFO Dokumen NFO Documento NFO NFO ドキュメント NFO құжаты NFO 문서 NFO dokumentas NFO dokuments NFO-dokument NFO-document NFO-dokument document NFO Dokument NFO documento NFO Documento NFO Document NFO документ NFO Dokument NFO Dokument NFO Dokument NFO документ НФО NFO-dokument NFO belgesi документ NFO Tài liệu NFO NFO 文档 NFO 文件 RPM spec file ملف مواصفات RPM Specyfikacyjny fajł RPM Файл — спецификация за RPM fitxer spec RPM soubor specifikace RPM RPM spec-fil RPM-Spezifikationsdatei Αρχείο spec RPM RPM spec file archivo de especificaciones RPM RPM espezifikazio fitxategia RPM spec -tiedosto RPM tøknilýsingarfíla fichier de spécification RPM comhad spec RPM ficheiro de especificacións RPM קובץ מפרט RPM RPM određena datoteka RPM spec fájl File de specification RPM Berkas spesifikasi RPM File specifica RPM RPM spec ファイル RPM анықтама файлы RPM spec 파일 RPM spec failas RPM specifikācijas datne RPM-spesifikasjonsfil RPM-spec-bestand RPM spec-fil fichièr d'especificacion RPM Plik spec RPM ficheiro de especificações RPM Arquivo de especificação RPM Fișier RPM spec файл описания RPM Súbor RPM spec Določilna datoteka RPM File specifikimi RPM РПМ посебна датотека RPM spec-fil RPM spec dosyası spec-файл RPM Tập tin đặc tả RPM RPM spec 文件 RPM spec 規格檔 RPM Red Hat Package Manager Sass CSS pre-processor file Scala source code Изходен код — Scala codi font en Scala zdrojový kód Scala Scala-kildekode Scala-Quelltext Πηγαίος κώδικας Scala Scala source code código fuente en Scala Scala iturburu-kodea Scala-lähdekoodi code source Scala código fnote en Scala קוד מקור של Scala Scala izvorni kod Scala forráskód Codice-fonte Scala Kode sumber Scala Codice sorgente Scala Scala ソースコード Scala-ის საწყისი კოდი Scala бастапқы коды Scala 소스 코드 Scala pirmkods Scala broncode còde font Scala Kod źródłowy Scala código origem Scala Código-fonte Scala исходный код Scala Zdrojový kód Scala Izvorna koda Scala Скала изворни ко̂д Scala-källkod Scala kaynak kodu вихідний код мовою Scala Scala 源代码 Scala 源碼 Scheme source code شفرة مصدر Scheme Sxem mənbə kodu Kryničny kod Scheme Изходен код — Scheme codi font en Scheme zdrojový kód Scheme Ffynhonnell Rhaglen Scheme Schemekildekode Scheme-Quelltext Πηγαίος κώδικας Scheme Scheme source code Scheme-fontkodo código fuente en Scheme Scheme iturburu-kodea Scheme-lähdekoodi Scheme keldukota code source Scheme cód foinseach Scheme código fonte en Scheme קוד מקור של Scheme Scheme izvorni kod Scheme-forráskód Codice-fonte Scheme Kode program Scheme Codice sorgente Scheme Scheme ソースコード Scheme бастапқы коды Scheme 소스 코드 Scheme pradinis kodas Scheme pirmkods Kod sumber Scheme Scheme-kildekode Scheme-broncode Scheme-kjeldekode còde font Scheme Kod źródłowy Scheme código origem Scheme Código-fonte Scheme Cod sursă Scheme исходный код Scheme Zdrojový kód Scheme Datoteka izvorne kode Scheme Kod burues Scheme Шемски изворни ко̂д Scheme-källkod Scheme kaynak kodu вихідний файл мовою Scheme Mã nguồn Scheme Scheme 源代码 Scheme 源碼 Sass CSS pre-processor file Setext document مستند Setext Setext sənədi Dakument Setext Документ — Setext document Setext dokument Setext Dogfen Setext Setextdokument Setext-Dokument Έγγραφο Setext Setext document Setext-dokumento documento Setext Setext dokumentua Setext-asiakirja Setext skjal document Setext cáipéis Setext documento Settext מסמך של Setext Setext dokument Setext-dokumentum Documento Setext Dokumen Setext Documento Setext Setext ドキュメント Setext құжаты Setext 문서 Setext dokumentas Setext dokuments Dokumen Setext Setext-dokument Setext-document Setext-dokument document Setext Dokument Setext documento Setext Documento Setext Document Setext документ Setext Dokument Setext Dokument Setext Dokument Setext Сетекст документ Setext-dokument Setext belgesi документ Setext Tài liệu Setext Setext 文档 Setext 文件 SQL code شفرة SQL SQL kodu Kod SQL Код — SQL codi en SQL kód SQL Côd SQL SQL-kode SQL-Befehle Κώδικας SQL SQL code SQL-kodo código SQL SQL kodea SQL-koodi SQL kota code SQL cód SQL código SQL קוד SQL SQL kod SQL-kód Codice SQL Kode SQL Codice SQL SQL コード SQL коды SQL 코드 SQL kodas SQL kods Kod SQL SQL-kildekode SQL-code SQL-kode còde SQL Kod SQL código SQL Código SQL Cod SQL код SQL Kód SQL Datoteka kode SQL Kod SQL СКуЛ ко̂д SQL-kod SQL kodu код SQL Mã SQL SQL 代码 SQL 程式碼 Tcl script سكربت Tcl Skrypt Tcl Скрипт — Tcl script Tcl skript Tcl Tcl-program Tcl-Skript Δέσμη ενεργειών Tcl Tcl script Tcl-skripto secuencia de órdenes en Tcl Tcl script-a Tcl-komentotiedosto Tcl boðrøð script Tcl script Tcl Script en Tcl תסריט Tcl Tcl skripta Tcl-parancsfájl Script Tcl Skrip Tcl Script Tcl Tcl スクリプト Tcl сценарийі Tcl 스크립트 Tcl scenarijus Tcl skripts Skrip Tcl Tcl-skript Tcl-script Tcl-skript escript Tcl Skrypt Tcl script Tcl Script Tcl Script Tcl сценарий Tcl Skript Tcl Skriptna datoteka Tcl Script Tcl Тцл скрипта Tcl-skript Tcl betiği скрипт Tcl Văn lệnh Tcl Tcl 脚本 Tcl 描述語言檔 TeX document مستند TeX Dakument TeX Документ — TeX document TeX dokument TeX Dogfen TeX TeX-dokument TeX-Dokument Έγγραφο TeX TeX document TeX-dokumento documento de TeX TeX dokumentua TeX-asiakirja TeX skjal document TeX cáipéis TeX documenton TeX מסמך TeX TeX dokument TeX-dokumentum Documento TeX Dokumen TeX Documento TeX TeX ドキュメント TeX құжаты TeX 문서 TeX dokumentas TeX dokuments Dokumen TeX TeX-dokument TeX-document TeX-dokument document TeX Dokument TeX documento TeX Documento TeX Document TeX документ TeX Dokument TeX Dokument TeX Dokument TeX ТеКс документ TeX-dokument TeX belgesi документ TeX Tài liệu TeX TeX 文档 TeX 文件 TeXInfo document مستند TeXInfo TeXInfo sənədi Dakument TeXInfo Документ — TeXInfo document TeXInfo dokument TeXInfo Dogfen TeXInfo TeXInfo-dokument TeXInfo-Dokument Έγγραφο TeXInfo TeXInfo document TeXInfo-dokumento documento de TeXInfo TeXInfo dokumentua TeXInfo-asiakirja TeXInfo skjal document TeXInfo cáipéis TeXInfo documento TeXInfo מסמך של TeXInfo TeXInfo dokument TeXInfo-dokumentum Documento TeXInfo Dokumen TeXInfo Documento TeXInfo TeXInfo ドキュメント TeXInfo құжаты TeXInfo 문서 TeXInfo dokumentas TeXInfo dokuments Dokumen TeXInfo TeXInfo-dokument TeXInfo-document TeXInfo-dokument document TeXInfo Dokument TeXInfo documento TeXInfo Documento TeXInfo Document TexInfo документ TeXInfo Dokument TeXInfo Dokument TeXInfo Dokument TeXInfo ТеКсинфо документ TeXInfo-dokument TeXInfo belgesi документ TeXInfo Tài liệu TeXInfo TeXInfo 文档 TeXInfo 文件 Troff ME input document مستند Troff ME input Uvodny dakument Troff ME Изходен документ — Troff ME document d'entrada Troff ME vstupní dokument Troff ME Troff ME inddata-dokument Troff-ME-Eingabedokument Έγγραφο εντολών troff ME Troff ME input document eniga dokumento de Troff ME documento de entrada Troff ME Troff ME sarrerako dokumentua Troff ME -syöteasiakirja Troff ME inntaksskjal document d'entrée Troff ME cáipéis ionchur Troff ME documento de entrada Troff ME מסמך קלט של Troff ME Troff ME ulazni dokument Troff ME bemeneti dokumentum Documento de entrata Troff ME Dokumen masukan Troff ME Documento di input Troff ME Troff ME 入力ドキュメント Troff ME кіріс құжаты Troff ME 입력 문서 Troff ME įvesties dokumentas Troff ME ievades dokuments Dokumen input Troff ME Troff ME-inndatadokument Troff ME-invoerdocument Troff ME inndata-dokument document d'entrada Troff ME Dokument wejściowy Troff ME documento origem Troff ME Documento de entrada Troff ME Document intrare Troff ME входной документ Troff ME Vstupný dokument Troff ME Vnosni dokument Troff ME Dokument i input Troff ME Трофф МЕ улазни документ Troff ME-indatadokument Troff ME girdi belgesi вхідний документ Troff ME Tài liệu nhập ME Troff Troff ME 输入文档 Troff ME 輸入文件 Troff MM input document مستند Troff MM input Uvodny dakument Troff MM Изходен документ — Troff MM document d'entrada Troff MM vstupní dokument Troff MM Troff MM inddata-dokument Troff-MM-Eingabedokument Έγγραφο εντολών troff MM Troff MM input document eniga dokumento de Troff MM documento de entrada Troff MM Troff MM sarrerako dokumentua Troff MM -syöteasiakirja Troff MM inntaksskjal document d'entrée Troff MM cáipéis ionchur Troff MM documento de entrada Troff MM מסמך קלט של Troff MM Troff MM ulazni dokument Troff MM bemeneti dokumentum Documento de entrata Troff MM Dokumen masukan Troff MM Documento di input Troff MM Troff MM 入力ドキュメント Troff MM кіріс құжаты Troff MM 입력 문서 Troff MM įvesties dokumentas Troff MM ievades dokuments Dokumen input Troff MM Troff MM-inndatadokument Troff MM-invoerdocument Troff MM inndata-dokument document d'entrada Troff MM Dokument wejściowy Troff MM documento origem Troff MM Documento de entrada Troff MM Document intrare Troff MM входной документ Troff MM Vstupný dokument Troff MM Vnosni dokument Troff MM Dokument i input Troff MM Трофф ММ улазни документ Troff MM-indatadokument Troff MM girdi belgesi вхідний документ Troff MM Tài liệu nhập MM Troff Troff MM 输入文档 Troff MM 輸入文件 Troff MS input document مستند Troff MS input Uvodny dakument Troff MS Изходен документ — Troff MS document d'entrada Troff MS vstupní dokument Troff MS Troff MS inddata-dokument Troff-MS-Eingabedokument Έγγραφο εντολών troff MS Troff MS input document eniga dokumento de Troff MS documento de entrada Troff MS Troff MS sarrerako dokumentua Troff MS -syöteasiakirja Troff MS inntaksskjal document d'entrée Troff MS cáipéis ionchur Troff MS documento de entrada Troff MS מסמך קלט של Troff MS Troff MS ulazni dokument Troff MS bemeneti dokumentum Documento de entrata Troff MS Dokumen masukan Troff MS Documento di input Troff MS Troff MS 入力ドキュメント Troff MS кіріс құжаты Troff MS 입력 문서 Troff MS įvesties dokumentas Troff MS ievades dokuments Dokumen input Troff MS Troff MS-inndatadokument Troff MS-invoerdocument Troff MS inndata-dokument document d'entrada Troff MS Dokument wejściowy Troff MS documento origem Troff MS Documento de entrada Troff MS Document intrare Troff MS входной документ Troff MS Vstupný dokument Troff MS Vnosni dokument Troff MS Dokument i input Troff MS Трофф МС улазни документ Troff MS-indatadokument Troff MS girdi belgesi вхідний документ Troff MS Tài liệu nhập MS Troff Troff MS 输入文档 Troff MS 輸入文件 Twig template X-Motif UIL table جدول X-Motif UIL Tablica X-Motif UIL Таблица — X-Motif UIL taula UIL de X-Motif tabulka X-Motif UIL X-Motif UIL-tabel X-Motif-UIL-Tabelle Πίνακας X-Motif UIL X-Motif UIL table tabla de X-Motif UIL X-Motif UIL taula X-Motif UIL -taulukko X-Motif UIL talva table X-Motif UIL tábla X-Motif UIL Táboa de X-Motif UIL טבלה של X-Motif UIL X-Motif UIL tablica X-Motif UIL-táblázat Tabella X-Motif UIL Tabel X-Motif UIL Tabella UIL X-Motif X-Motif UIL 表 X-Motif UIL кестесі X-Motif UIL 테이블 X-Motif UIL lentelė X-Motif UIL tabula Jadual X-Motif UIL X-Motif UIL-tabell X-Motif UIL-tabel X-Motif UIL-tabell taula X-Motif UIL Tabela UIL X-Motif tabela UIL do X-Motif Tabela UIL do X-Motif Tabel X-Motif UIL таблица UIL X-Motif Tabuľka X-Motif UIL Preglednica X-Motif UIL Tabelë X-Motif UIL Икс-Мотиф УИЛ табела X-Motif UIL-tabell X-Motif UIL tablosu таблиця X-Motif UIL Bảng UIL X-Motif X-Motif UIL 表 X-Motif UIL 表格 resource location موقع المورد pałažeńnie resursu Местоположение на ресурс localització de recurs umístění prostředku resurseplacering Ressourcenort Τοποθεσία πόρου resource location loko de risurco ubicación del recurso baliabidearen kokalekua resurssisijainti tilfeingisstaður localisation de ressource suíomh acmhainne localización do recurso מיקום של משאב položaj resursa erőforrás-hely Loco de ressources lokasi sumber daya Posizione risorsa リソースの場所 ресурс орналасуы 자원 위치 resurso vieta resursa atrašanās vieta Lokasi sumber ressurslokasjon bronlocatie ressursplassering localizacion de ressorsa Położenie zasobu localização de recurso Localização de recurso locație de resursă расположение ресурса Umiestnenie zdroja mesto vira Pozicion rezerve путања изворишта resursplats kaynak ayırma розташування ресурсу địa điểm tài nguyên 资源位置 資源位置 uuencoded file fitxer uuencoded soubor kódovaný pomocí uuencoding uuencodede-fil Datei im uuencode-Format Αρχείο κωδικοποιημένο unix σε unix (uuencoded) uuencoded file archivo codificado con uuencode uuencode-aturiko fitxategia fichier uuencodé Ficheiro uuencoded קובץ בקידוד uu uuencoded datoteka uuencode-olt fájl File in uuencode Berkas ter-uuencode File uuencoded 未エンコードファイル uuencode кодталған файлы uuencoded 파일 uu kodējuma datne fichièr uuencodat Plik zakodowany za pomocą uuencode ficheiro uuencoded Arquivo codificado UUE файл в кодировке uuencode Súbor v kódovaní uuencode Datoteka uuencode уукодирана датотека uuencode-fil uuencoded dosyası файл даних у форматі UUE 未编码的文件 uuencoded 檔 XMI file ملف XMI Fajł XMI Файл — XMI fitxer XMI soubor XMI XMI-fil XMI-Datei Αρχείο XML XMI file XMI-dosiero archivo XMI XMI fitxategia XMI-tiedosto XMI fíla fichier XMI comhad XMI ficheiro XMI קובץ XMI XMI datoteka XMI fájl File XMI Berkas XMI File XMI XMI ファイル XMI файлы XMI 파일 XMI failas XMI datne XMI-fil XMI-bestand XMI-fil fichièr XMI Plik XMI ficheiro XMI Arquivo XMI Fișier XMI файл XMI Súbor XMI Datoteka XMI File XMI ИксМИ датотека XMI-fil XMI dosyası файл XMI Tập tin XMI XMI 文件 XMI 檔 XMI XML Metadata Interchange XSL FO file ملف XSL FO Fajł XSL FO Форматиращ файл — XSL FO fitxer FO XSL soubor XSL FO XML FO-fil XSL-FO-Datei Αρχείο XSL FO XSL FO file XSL-FO-dosiero archivo XSL FO XSL FO fitxategia XSL FO -tiedosto XSL FO fíla fichier XSL FO comhad XSL FO ficheiro XSL FO קובץ XSL FO XSL FO datoteka XSL FO fájl File XSL FO Berkas XSL FO File XSL FO XSL FO ファイル XSL FO файлы XSL 포매팅 개체 파일 XSL FO failas XSL FO datne FO-fil for XSL XSL FO-bestand XSL FO-fil fichièr XSL FO Plik XSL FO ficheiro XSL FO Arquivo XSL FO Fișier XSL FO файл XSL FO Súbor XSL FO Datoteka XSL FO File XSL FO ИксСЛ ФО датотека XSL FO-fil XSL FO dosyası файл XSL FO Tập tin FO của XSL (XFO) XSL 格式化对象文件 XSL FO 檔 XSL FO XSL Formatting Objects iptables configuration file ملف تضبيط iptables kanfihuracyjny fajł iptables Настройки за iptables fitxer de configuració d'iptables soubor nastavení iptables iptableskonfigurationsfil iptables-Konfigurationsdatei Αρχείο ρυθμίσεων iptables iptables configuration file archivo de configuración de iptables iptables konfigurazio-fitxategia iptables-asetustiedosto iptables samansetingarfíla fichier de configuration iptables comhad cumraíochta iptables ficheiro de configuración de iptables קובץ הגדרה של iptables iptables datoteka s postavkama iptables beállítófájl File de configuration IPTables berkas konfigurasi iptables File configurazione iptables iptables 設定ファイル iptables баптаулар файлы iptables 설정 파일 iptables konfigūracijos failas iptables konfigurācijas datne konfigurasjonsfil for iptables iptables-configuratiebestand iptables oppsettfil fichièr de configuracion iptables Plik konfiguracji iptables ficheiro de configuração iptables Arquivo de configuração do iptables fișier configurare iptables файл настроек iptables Súbor nastavení iptables nastavitvena datoteka iptables File konfigurimi iptables датотека подешавања иптабела iptables-konfigurationsfil iptables yapılandırma dosyası файл налаштувань iptables tập tin cấu hình iptables iptables 防火墙配置文件 iptables 組態檔 XSLT stylesheet نمط XSLT Arkuš stylaŭ XSLT Стилове — XSLT full d'estil XSLT stylopis XSLT XSLT-stilark XSLT-Stylesheet Φύλλο στυλ XSLT XSLT stylesheet XSLT-stilfolio hoja de estilos XSLT XSLT estilo-orria XSLT-tyylitiedosto XSLT sniðark feuille de style XSLT stílbhileog XSLT folla de estilo XSLT גליון סגנון XSLT XSLT stilska tablica XSLT-stíluslap Folio de stilo XSLT Lembar gaya XSLT Foglio di stile XSLT XSLT スタイルシート XSLT стильдер кестесі XSLT 스타일시트 XSLT stiliaus aprašas XSLT izklājlapa Helaian Gaya XSLT XSLT-stilark XSLT-stijlblad XSLT-stilark fuèlh d'estil XSLT Arkusz stylów XSLT folha de estilos XSLT Folha de estilo XSLT Fișă de stil XSLT таблица стилей XSLT Štýl XSLT Slogovna predloga XSLT Fletë stili XSLT ИксСЛТ стилски лист XSLT-stilmall XSLT çalışma sayfası таблиця стилів XSLT Tờ kiểu dáng XSLT XSLT 样式表 XSLT 樣式表 XSLT eXtensible Stylesheet Language Transformation XMCD CD database قاعدة بيانات XMCD CD Baza źviestak ab dyskach XMCD База от данни за CD-та — XMCD base de dades de CD XMCD databáze XMCD CD XMCD-cd-database XMCD-CD-Datenbank Βάση δεδομένων CD XMCD XMCD CD database base de datos de CD XMCD XMCD CD datu-basea XMCD CD -tietokanta XMCD fløgu dátustovnur base de données de CD XMCD bunachar sonraí XMCD CD base de datos de CD XMCD מסד נתונים XMCD CD XMCD CD baza podataka XMCD CD-adatbázis Base de datos de CD XMCD Basis data XMCD CD Database XMCD CD XMCD CD データベース XMCD CD дерекқоры XMCD CD 데이터베이스 XMCD CD duomenų bazė XMCD CD datubāze XMCD CD-database XMCD CD-gegevensbank XMCD CD-database banca de donadas de CD XMCD Baza danych CD XMCD base de dados XMCD CD Banco de dados de CD XMCD Bază de date XMCD CD база данных компакт-дисков XMCD Databáza XMCD CD Podatkovna zbirka XMCD CD Bazë me të dhëna XMCD CD ИксМЦД ЦД база података XMCD cd-databas XMCD CD veritabanı база даних XMCD CD Cơ sở dữ liệu CD XMCD XMCD CD 数据库 XMCD CD 資料庫 XML document مستند XML Dakument XML Документ — XML document XML dokument XML XML-dokument XML-Dokument Έγγραφο XML XML document XML-dokumento documento XML XML dokumentua XML-asiakirja XML skjal document XML cáipéis XML documento XML מסמך XML XML dokument XML dokumentum Documento XML Dokumen XML Documento XML XML ドキュメント XML құжаты XML 문서 XML dokumentas XML dokuments XML-dokument XML-document XML-dokument document XML Dokument XML documento XML Documento XML Document XML документ XML Dokument XML Dokument XML Dokument XML ИксМЛ документ XML-dokument XML belgesi документ XML Tài liệu XML XML 文档 XML 文件 XML eXtensible Markup Language XML entities document مستند كيانات XML Dakument elementaŭ XML Документ — заместващи последователности в XML document d'entitats XML dokument entit XML XML-enhedsdokument XML-Dokument-Entitäten Έγγραφο οντοτήτων XML XML entities document documento de entidades XML XML entitateen dokumentua XML-entiteettiasiakirja XML einindisskjal document d'entités XML cáipéis aonán XML documento de entidades XML מסמך ישויות XML Dokument XML subjekata XML egyeddokumentum Documento de entitates XML Dokumen entitas XML Documento entità XML XML エントリドキュメント XML мәндер құжаты XML 엔티티 문서 XML esybių dokumentas XML vienību dokuments XML-entitetsdokument XML entiteiten-document XML-entitet-dokument document d'entitats XML Dokument jednostek XML documento de entidades XML Documento de entidades XML Document entități XML файл сущностей XML Dokument entít XML Dokument XML določil Dokument njësish XML документ ИксМЛ ставки XML-entitetsdokument XML varlıklar belgesi документ об’єктів XML Tài liệu thực thể XML XML 特征文档 XML 實體文件 XML eXtensible Markup Language DV video DV مرئي Videa DV Видео — DV vídeo DV video DV DV-video DV-Video Βίντεο DV DV video DV-video vídeo DV DV bideoa DV-video DV video vidéo DV físeán DV vídeo DV וידאו DV DV video DV videó Video DV Video DV Video DV DV 動画 DV ვიდეო DV видеосы DV 동영상 DV vaizdo įrašas DV video DV-film DV-video DV-video vidèo DV Plik wideo DV vídeo DV Vídeo DV Video DV видео DV Video DV Video datoteka DV Video DV ДВ видео DV-video DV video відеокліп DV Ảnh động DV DV 视频 DV 視訊 DV Digital Video ISI video مرئي ISI ISI video faylı Videa ISI Видео — ISI vídeo ISI video ISI Fideo ISI ISI-video ISI-Video Βίντεο ISI ISI video ISI-video vídeo ISI ISI bideoa ISI-video ISI video vidéo ISI físeán ISI vídeo ISI וידאו ISI ISI video ISI-videó Video ISI Video ISI Video ISI ISI 動画 ISI видеосы ISI 동영상 ISI vaizdo įrašas ISI video Video ISI ISI-film ISI-video ISI video vidèo ISI Plik wideo ISI vídeo ISI Vídeo ISI Video ISI видео ISI Video ISI Video datoteka ISI Video ISI ИСИ видео ISI-video ISI videosu відеокліп ISI Ảnh động ISI ISI 视频 ISI 視訊 MPEG-2 transport stream بث نقل MPEG-2 Поток — транспорт по MPEG-2 flux de transport MPEG-2 přenosový proud MPEG-2 MPEG-2-transportstrøm MPEG-2-Transportstrom Ροή μεταφοράς MPEG-2 MPEG-2 transport stream flujo de transporte MPEG-2 MPEG-2 korronte garraioa MPEG-2 -siirtobittivirta MPEG-2 flutningsstreymur flux de transport MPEG-2 Sruth aistrithe MPEG-2 fluxo de transporte MPEG-2 העברת זרימה של MPEG-2 MPEG-2 transportni tok MPEG-2 átviteli adatfolyam Fluxo de transporto MPEG-2 Stream transport MPEG-2 Stream di trasporto MPEG-2 MPEG-2 トランスポートストリーム MPEG-2-ის ტრანსპორტული ნაკადი MPEG-2 көліктік ағыны MPEG-2 전송 스트림 MPEG-2 transportavimo srautas MPEG-2 transporta straume MPEG-2 transport stream flux de transpòrt MPEG-2 Strumień przesyłania MPEG-2 fluxo de transporte MPEG-2 Fluxo de transporte de MPEG-2 Flux transport MPEG-2 транспортный поток MPEG-2 MPEG-2 Transport Stream Pretočni vir prenosega MPEG МПЕГ-2 ток преноса MPEG-2 transportström MPEG-2 aktarım akışı потік передавання даних MPEG-2 MPEG-2 传输流 MPEG-2 傳輸串流 MPEG-2 TS Moving Picture Experts Group 2 Transport Stream MPEG video MPEG مرئي Videa MPEG Видео — MPEG vídeo MPEG video MPEG MPEG-video MPEG-Video Βίντεο MPEG MPEG video MPEG-video vídeo MPEG MPEG bideoa MPEG-video MPEG video vidéo MPEG físeán MPEG vídeo MPEG וידאו MPEG MPEG video MPEG-videó Video MPEG Video MPEG Video MPEG MPEG 動画 MPEG ვიდეო MPEG видеосы MPEG 동영상 MPEG vaizdo įrašas MPEG video Video MPEG MPEG-film MPEG-video MPEG-video vidèo MPEG Plik wideo MPEG vídeo MPEG Vídeo MPEG Video MPEG видео MPEG Video MPEG Video datoteka MPEG Video MPEG МПЕГ видео MPEG-video MPEG videosu відеокліп MPEG Ảnh động MPEG MPEG 视频 MPEG 視訊 MPEG Moving Picture Experts Group MPEG video (streamed) Видео — MPEG, поточно vídeo MPEG (flux) video MPEG (proud) MPEG-video (streamet) MPEG-Video (Datenstrom) Βίντεο MPEG (εκπεμπόμενο) MPEG video (streamed) vídeo MPEG (transmisión) MPEG bideoa (korronte bidez) MPEG-video (virtaus) vidéo MPEG (flux) vídeo MPEG (en stream) קובץ MPEG (בהזרמה) MPEG video snimka (strujanje) MPEG videó (szórt) Video MPEG (in fluxo) Video MPEG (di-stream-kan) Video MPEG (streamed) MPEG ビデオ(ストリーム) MPEG ვიდეო (ნაკადი) MPEG видео (ағымдық) MPEG 동영상(스트리밍) MPEG video (straumēts) MPEG video (streamed) vidèo MPEG (flux) Plik wideo MPEG (strumień) vídeo MPEG (em fluxo) Vídeo MPEG (fluxo) видео MPEG (потоковое) MPEG video (streamované) MPEG-video (pretočni) МПЕГ видео (проточни) MPEG-video (strömmad) MPEG videosu (akış) відеокліп MPEG (потоковий) MPEG 视频流媒体 MPEG 視訊 (串流) QuickTime video QuickTime مرئي Videa QuickTime Видео — QuickTime vídeo QuickTime video QuickTime QuickTime-video QuickTime-Video Βίντεο QuickTime QuickTime video QuickTime-video vídeo QuickTime QuickTime bideoa QuickTime-video QuickTime video vidéo QuickTime físeán QuickTime vídeo QuickTime וידאו של QuickTime QuickTime video QuickTime videó Video QuickTime Video QuickTime Video QuickTime QuickTime 動画 QuickTime видеосы 퀵타임 동영상 QuickTime vaizdo įrašas QuickTime video Video QuickTime Quicktime film QuickTime-video QuickTime-video vidèo QuickTime Plik wideo QuickTime vídeo QuickTime Vídeo do QuickTime Video QuickTime видео QuickTime Video QuickTime Video datoteka QuickTime Video QuickTime Квик Тајм видео QuickTime-video QuickTime videosu відеокліп QuickTime Ảnh động QuickTime QuickTime 视频 QuickTime 視訊 QuickTime image صورة QuickTime Vyjava QuickTime Изображение — QuickTime imatge QuickTime obrázek QuickTime QuickTime-billede QuickTime-Bild Εικόνα QuickTime QuickTime image QuickTime-bildo imagen de QuickTime QuickTime irudia QuickTime-kuva QuickTime mynd image QuickTime íomhá QuickTime imaxe QuickTime תמונה של QuickTime QuickTime slika QuickTime kép Imagine QuickTime Citra QuickTime Immagine QuickTime QuickTime 画像 QuickTime суреті 퀵타임 그림 QuickTime paveikslėlis QuickTime attēls Quicktime bilde QuickTime-afbeelding QuickTime-bilete imatge QuickTime Obraz QuickTime imagem QuickTime Imagem do QuickTime Imagine QuickTime изображение QuickTime Obrázok QuickTime Slikovna datoteka QuickTime Figurë QuickTime Квик Тајм слика QuickTime-bild QuickTime görüntüsü зображення QuickTime Ảnh QuickTime QuickTime 图像 QuickTime 影像 Vivo video Vivo مرئي Vivo video faylı Videa Vivo Видео — Vivo vídeo Vivo video Vivo Fideo Vivo Vivo-video Vivo-Video Βίντεο Vivo Vivo video Vivo-video vídeo Vivo Vivo bideoa Vivo-video Vivo video vidéo Vivo físeán Vivo vídeo Vivo וידאו של Vivo Vivo video Vivo-videó Video Vivo Video Vivo Video Vivo Vivo 動画 Vivo видеосы Vivo 동영상 Vivo vaizdo įrašas Vivo video Video Vivo Vivo-film Vivo-video Vivo-film vidèo Vivo Plik wideo Vivo vídeo Vivo Vídeo Vivo Video Vivo видео Vivo Video Vivo Video datoteka Vivo Video Vivo Виво видео Vivo-video Vivo videosu відео Vivo Ảnh động Vivo Vivo 视频 Vivo 視訊 Wavelet video Wavelet مرئي Wavelet video faylı Videa Wavelet Видео — Wavelet vídeo Wavelet video Wavelet Fideo Wavelet Waveletvideo Wavelet-Video Βίντεο Wavelet Wavelet video Wavelet-video vídeo Wavelet Wavelet bideoa Wavelet-video Wavelet video vidéo Wavelet físeán Wavelet vídeo Wavelet וידאו של Wavelet Wavelet video Wavelet-videó Video Wavelet Video Wavelet Video Wavelet Wavelet 動画 Wavelet видеосы Wavelet 동영상 Wavelet vaizdo įrašas Wavelet video Video Wavelet Wavelet-film Wavelet-video Wavelet video vidèo Wavelet Plik wideo Wavelet vídeo Wavelet Vídeo Wavelet Video Wavelet видео Wavelet Video Wavelet Video datoteka Wavelet Video Wavelet Вејвелет видео Wavelet-video Wavelet videosu відеокліп Wavelet Ảnh động Wavelet Wavelet 视频 Wavelet 視訊 ANIM animation تحريكة ANIM ANIM animasiyası Animacyja ANIM Анимация — ANIM animació ANIM animace ANIM Animeiddiad ANIM ANIM-animation ANIM-Animation Κινούμενο σχέδιο ANIM ANIM animation ANIM-animacio animación ANIM ANIM animazioa ANIM-animaatio ANIM teknmyndagerð animation ANIM beochan ANIM animación ANIM הנפשת ANIM ANIM animacija ANIM-animáció Animation ANIM Animasi ANIM Animazione ANIM ANIM アニメーション ANIM ანიმაცია ANIM анимациясы ANIM 동화상 ANIM animacija ANIM animācija Animasi ANIM ANIM-animasjon ANIM-animatie ANIM-animasjon animacion ANIM Plik animacji ANIM animação ANIM Animação ANIM Animație ANIM анимация ANIM Animácia ANIM Datoteka animacije ANIM Animim ANIM АНИМ анимација ANIM-animering ANIM canlandırması анімація ANIM Hoạt ảnh ANIM ANIM 动画 ANIM 動畫 FLIC animation تحريكة FLIC Animacyja FLIC Анимация — FLIC animació FLIC animace FLIC FLIC-animation FLIC-Animation Κινούμενο σχέδιο FLIC FLIC animation animación FLIC FLIC animazioa FLIC-animaatio FLIC teknimyndagerð animation FLIC beochan FLIC animación FLIC הנפשת FLIC FLIC animacija FLIC animáció Animation FLIC Animasi FLIC Animazione FLIC FLIC アニメーション FLIC ანიმაცია FLIC анимациясы FLIC 동화상 FLIC animacija FLIC animācija FLIC-animasjon FLIC-animatie FLIC-animasjon animacion FLIC Plik animacji FLIC animação FLIC Animação FLIC Animație FLIC анимация FLIC Animácia FLIC Datoteka animacije FLIC Animim FLIC ФЛИЦ анимација FLIC-animering FLIC animasyonu анімація FLIC Hoạt ảnh FLIC FLIC 动画 FLIC 動畫 Haansoft Hangul document مستند Haansoft Hangul Dakument Haansoft Hangul Документ — Haansoft Hangul document d'Haansoft Hangul dokument Haansoft Hangul Haansoft Hangul-dokument Haansoft-Hangul-Dokument Έγγραφο Haansoft Hangul Haansoft Hangul document documento de Haansoft Hangul Haansoft Hangul dokumentua Haansoft Hangul -asiakirja Haansoft Hangul skjal document Haansoft Hangul cáipéis Haansoft Hangul documento de Haansoft Hangul מסמך Haansoft Hangul Haansoft Hangul dokument Haansoft hangul dokumentum Documento Haansoft Hangul Dokumen Haansoft Hangul Documento Haansoft Hangul Haansoft Hangul ドキュメント Haansoft Hangul құжаты 한소프트 한글 문서 Haansoft Hangul dokumentas Haansoft Hangul dokuments Haansoft Hangul-dokument Haansoft Hangul-document Haansoft Hangul-dokument document Haansoft Hangul Dokument Haansoft Hangul documento Haansoft Hangul Documento do Haansoft Hangul Document Haansoft Hangul документ Haansoft Hangul Dokument Haansoft Hangul Dokument Haansoft Hangul Dokument Haansoft Hangul Хансофт Хангул документ Haansoft Hangul-dokument Haansoft Hangul belgesi документ Haansoft Hangul Tài liệu Hangul Haansoft Haansoft Hangul 文档 Haansoft 韓文文件 Haansoft Hangul document template قالب مستند Haansoft Hangul Šablon dakumentu Haansoft Hangul Шаблон за документи — Haansoft Hangul plantilla de document d'Haansoft Hangul šablona dokumentu Haansoft Hangul Haansoft Hangul-dokumentskabelon Haansoft-Hangul-Dokumentvorlage Πρότυπο εγγράφου Haansoft Hangul Haansoft Hangul document template plantilla de documento de Haansoft Hangul Haansoft Hangul dokumentuaren txantiloia Haansoft Hangul -asiakirjamalli Haansoft Hangul skjalaformur modèle de document Haansoft Hangul teimpléad cháipéis Haansoft Hangul modelo de documento de Haansoft Hangul תבנית מסמך של Haansoft Hangul Haansoft Hangul predložak dokumenta Haansoft hangul dokumentumsablon Patrono de documento Haansoft Hangul Templat dokumen Haansoft Hangul Modello documento Haansoft Hangul Haansoft Hangul ドキュメントテンプレート Haansoft Hangul құжат үлгісі 한소프트 한글 문서 서식 Haansoft Hangul dokumento šablonas Haansoft Hangul dokumentu veidne Haansoft Hangul-dokumentmal Haansoft Hangul-documentsjabloon Haansoft Hangul-dokumentmal modèl de document Haansoft Hangul Szablon dokumentu Haansoft Hangul modelo de documento Haansoft Hangul Modelo de documento do Haansoft Hangul Document șablon Haansoft Hangul шаблон документа Haansoft Hangul Šablóna dokumentu Haansoft Hangul Predloga dokumenta Haansoft Hangul Model dokumenti Haansoft Hangul шаблон Хансофт Хангул документа Haansoft Hangul-dokumentmall Haansoft Hangul belge şablonu шаблон документа Haansoft Hangul Mẫu tài liệu Hangul Haansoft Haansoft Hangul 文档模板 Haansoft 韓文文件範本 MNG animation تحريكة MNG Animacyja MNG Анимация — MNG animació MNG animace MNG MNG-animation MNG-Animation Κινούμενο σχέδιο MNG MNG animation MNG-animacio animación MNG MNG animazioa MNG-animaatio MNG teknimyndagerð animation MNG beochan MNG animación MNG הנפשת MNG MNG animacija MNG-animáció Animation MNG Animasi MNG Animazione MNG MNG アニメーション MNG анимациясы MNG 동화상 MNG animacija MNG animācija Animasi MNG MNG-animasjon MNG-animatie MNG-animasjon animacion MNG Animacja MNG animação MNG Animação MNG Animație MNG анимация MNG Animácia MNG Datoteka animacije MNG Animim MNG МНГ анимација MNG-animering MNG canlandırması анімація MNG Hoạt ảnh MNG MNG 动画 MNG 動畫 MNG Multiple-Image Network Graphics ASF video ASF مرئي Videa ASF Видео — ASF vídeo ASF video ASF ASF-video ASF-Video Βίντεο ASF ASF video ASF-video vídeo ASF ASF bideoa ASF-video ASF video vidéo ASF físeán ASF vídeo ASF וידאו ASF ASF video ASF videó Video ASF Video ASF Video ASF ASF 動画 ASF ვიდეო ASF видеосы ASF 동영상 ASF vaizdo įrašas ASF video ASF-film ASF-video ASF-video vidèo ASF Plik wideo ASF vídeo ASF Vídeo ASF Video ASF видео ASF Video ASF Video datoteka ASF Video ASF АСФ видео ASF-video ASF videosu відеокліп ASF Ảnh động ASF ASF 视频 ASF 視訊 ASF Advanced Streaming Format Windows Media Station file ملف محطة Windows Media Fajł Windows Media Station Файл — Windows Media Station fitxer de Windows Media Station soubor Windows Media Station Windows Media Station-fil Windows-Media-Streamingbeschreibung Αρχείο Windows Media Station Windows Media Station file archivo de emisora de Windows Media Windows Media Station fitxategia Windows Media Station-tiedosto Windows Media Station fíla fichier Windows Media Station comhad Windows Media Station ficheiro de emisora de Windows Media קובץ תחנה של Windows Media Windows Media Station datoteka Windows Media Station fájl File de station Windows Media Berkas Windows Media Station File Windows Media Station Windows Media Station ファイル Windows Media Station файлы Windows Media Station 파일 Windows Media Station failas Windows Media Station datne Windows Media Station-fil Windows Media Station-bestand Windows Media Station-fil fichièr Windows Media Station Plik Windows Media Station ficheiro Windows Media Station Arquivo de estação do Windows Media Fișier Windows Media Station файл Windows Media Station Súbor Windows Media Station Datoteka Windows Media Station File Windows Media Station датотека станице Виндоузовог Медија Windows Media Station-fil Windows Media Station dosyası файл Windows Media Station Tập tin Windows Media Station Windows 媒体工作站文件 Windows Media Station 檔 Windows Media video Windows Media مرئي Videa Windows Media Видео — Windows Media vídeo de Windows Media video Windows Media Windows Medie-video Windows-Media-Video Βίντεο Windows Media Windows Media video vídeo de Windows Media Windows Media bideoa Windows Media -video Windows Media video vidéo Windows Media físeán Windows Media vídeo de Windows Media וידאו של Windows Media Windows Media video Windows Media videó Video Windows Media Video Windows Media Video Windows Media Windows Media 動画 Windows Media видеосы Windows 미디어 오디오 Windows Media vaizdo įrašas Windows Media video Windows Media film Windows Media-video Windows Media-video vidèo Windows Media Plik wideo Windows Media vídeo Windows Media Vídeo do Windows Media Video Windows Media видео Windows Media Video Windows Media Video datoteka Windows Media Video Windows Media Виндоуз Медија видео Windows Media-video Windows Media videosu відеокліп Windows Media Ảnh động Windows Media Windows Media 视频 Windows Media 視訊 AVI video AVI مرئي AVI video faylı Videa AVI Видео — AVI vídeo AVI video AVI Fideo AVI AVI-video AVI-Video Βίντεο AVI AVI video AVI-video vídeo AVI AVI bideoa AVI-video AVI video vidéo AVI físeán AVI vídeo AVI וידאו AVI AVI video AVI-videó Video AVI Video AVI Video AVI AVI 動画 AVI ვიდეო AVI видеосы AVI 동영상 AVI vaizdo įrašas AVI video Video AVI AVI-film AVI-video AVI-video vidèo AVI Plik wideo AVI vídeo AVI Vídeo AVI Video AVI видео AVI Video AVI Video datoteka AVI Video AVI АВИ видео AVI-video AVI videosu відеокліп AVI Ảnh động AVI AVI 视频 AVI 視訊 AVI Audio Video Interleave NullSoft video NullSoft مرئي Videa NullSoft Видео — NullSoft vídeo NullSoft video NullSoft NullSoft-video NullSoft-Video Βίντεο Nullsoft NullSoft video NullSoft-video vídeo NullSoft NullSoft bideoa NullSoft-video NullSoft video vidéo NullSoft físeán NullSoft vídeo de NullSoft וידאו של NullSot NullSoft video NullSoft videó Video NullSoft Video NullSoft Video NullSoft NullSoft 動画 NullSoft видеосы 널소프트 동영상 NullSoft vaizdo įrašas NullSoft video Nullsoft-film NullSoft-video NullSoft-video vidèo NullSoft Plik wideo NullSoft vídeo NullSoft Vídeo do NullSoft Video NullSoft видео Nullsoft Video NullSoft Video datoteka NullSoft Video NullSoft Нул Софт видео NullSoft-video Nullsoft videosu відеокліп NullSoft Ảnh động NullSoft Nullsoft 视频 NullSoft 視訊 SDP multicast stream file ملف دفق متعدد البث SDP Šmatadrasny płynievy fajł SDP Файл за поток — SDP multicast fitxer de flux de multidifusió SDP soubor vícesměrového vysílání proudu SDP SDP multicast-strømfil SDP-Multicast-Datenstromdatei Αρχείο ροής πολλαπλής αναμετάδοσης SDP SDP multicast stream file archivo de flujo multicast SDP SDP multicast korrontearen fitxategia SDP-monilähetysvirran tiedosto SDP margvarpað streymafíla fichier de flux multidiffusion SDP comhad shruth ilchraolacháin SDP ficheiro de fluxo multicast SDP קובץ שידור בזרימה SDP SDP datoteka strujanja emitiranja SDP multicast műsorfájl File de fluxo multidiffusion SDP Berkas SDP multicast stream File stream multicast SDP SDP マルチキャストストリームファイル SDP мультикаст ағым файлы SDP 멀티캐스트 스트림 파일 SDP daugiaadresio srauto failas SDP multiraides straumes datne SDP-multicaststrøm SDP-multicast-streambestand SDP multicast straumfil fichièr de flux multidifusion SDP Plik strumienia multicast SDP ficheiro de fluxo SDP multicast Arquivo de canal multicast SDP Fișier flux multicast SDP файл мультикаст-потока SDP Súbor viacsmerového vysielania prúdu SDP Pretočni vir večsmernega oddajanja File stream multicast SDP СДП датотека тока вишеструког емитовања SDP multicast stream-fil SDP çoklu yayın akışı dosyası файл потокової трансляції SDP Tập tin luồng truyền một-nhiều SDP SDP 多播流文件 SDP multicast 串流檔 SDP Session Description Protocol SGI video SGI مرئي SGI video faylı Videa SGI Видео — SGI vídeo SGI video SGI Video SGI SGI-video SGI-Video Βίντεο SGI SGI video SGI-video vídeo SGI SGI bideoa SGI-video SGI video vidéo SGI físeán SGI vídeo SGI וידאו SGI SGI video SGI-videó Video SGI Video SGI Video SGI SGI 動画 SGI видеосы SGI 동영상 SGI vaizdo įrašas SGI video Video SGI SGI-film SGI-video SGI-video vidèo SGI Plik wideo SGI vídeo SGI Vídeo SGI Video SGI видео SGI Video SGI Video datoteka SGI Video SGI СГИ видео SGI-video SGI videosu відеокліп SGI Ảnh động SGI SGI 视频 SGI 視訊 eMusic download package حزمة تنزيل eMusic pakunak zahruzki eMusic Пакет за сваляне — eMusic paquet de descàrrega eMusic balíček stahování eMusic eMusic-hentpakke eMusic-Download-Paket Πακέτο λήψης eMusic eMusic download package paquete de descarga eMusic eMusic deskargaren paketea eMusic-imurointipaketti eMusic niðurtøkupakki paquet de téléchargement eMusic pacáiste íosluchtú eMusic paquete de descarga de eMusic חבילת הורדה של eMusic eMusic preuzeti paket eMusic letöltési csomag Pacchetto de discargamento eMusic paket unduh eMusic Pacchetto scaricamento eMusic eMusic ダウンロードパッケージ eMusic жүктемелер дестесі eMusic 다운로드 패키지 eMusic atsiuntimo paketas eMusic lejupielādes paciņa eMusic nedlastingspakke eMusic-downloadpakket eMusic nedlastingspakke paquet de telecargament eMusic Pobrany pakiet eMusic pacote transferido eMusic Pacote de download do eMusic pachet descărcare eMusic пакет загрузок eMusic Balíček sťahovania eMusic Datoteka paketa eMusic Paketë shkarkimi eMusic пакет преузимања еМузике eMusic-hämtningspaket eMusic indirme paketi пакунок завантаження eMusic gói nhạc tải xuống eMusic eMusic 下载包 eMusic 下載包 KML geographic data بيانات جغرافية KML Географски данни — KML dades geogràfiques KML geografická data KML Geografiske data i KML-format KML geographische Daten Γεωγραφικά δεδομένα KML KML geographic data datos geográficos KML KML datu geografikoak KML-paikkatieto KML landafrøðilig dáta données géographiques KML sonraí geografacha KML datos xeográficos KML מידע גאוגרפי KML KML geografski podaci KML földrajzi adatok Datos geographic KML Data geografis KML Dati geografici KML KML 地理データ KML географилық ақпараты KML 지리 정보 데이터 KML geografiniai duomenys KML ģeogrāfiskie dati KML geographic data donadas geograficas KML Dane geograficzne KML dados geográficos KML Dados geográficos KML Date geografice KML географические данные KML Zemepisné údaje KML Datoteka geografskih podatkov KML КМЛ географски подаци KML geografisk data KML coğrafi verisi географічні дані KML KML 地理数据 KML 地理資料 KML Keyhole Markup Language KML geographic compressed data بيانات جغرافية مضغوطة KML Географски данни — KML, компресирани dades geogràfiques KML amb compressió komprimovaná geografická data KML KML-geografiske komprimerede data KML geographische komprimierte Daten Γεωγραφικά συμπιεσμένα δεδομένα KML KML geographic compressed data datos geográficos comprimidos KML KML datu geografiko konprimituak Pakattu KML-paikkatieto KML landafrøðilig stappað dáta données géographiques KML compressées sonraí comhbhrúite geografacha KML datos xeográficos KML comprimidos מידע גאוגרפי דחוס KML KML geografski komprimirani podaci KML tömörített földrajzi adatok Datos geographic KML comprimite Data geografis KML terkompresi Dati geografici KML compressi KML 地理圧縮データ KML географиялық сығылған ақпарат KML 지리 정보 압축 데이터 KML geografiniai suglaudinti duomenys KML saspiesti ģeogrāfiskie dati KML geographic compressed data donadas geograficas KML compressats Skompresowane dane geograficzne KML dados geográficos comprimidos KML Dados geográficos KML compactados Date geografice comprimate KML сжатые географические данные KML Komprimované zemepisné údaje KML Skrčeni geografski podatki KML КМЛ географски запаковани подаци KML geografiskt komprimerat data KML sıkıştırılmış coğrafi verisi стиснуті географічні дані KML KML 压缩地理数据 KML 地理壓縮資料 KML Keyhole Markup Language GeoJSON geospatial data dades geomàtiques GeoJSON geoprostorová data GeoJSON GeoJSON raumbezogene Daten GeoJSON geospatial data datos geoespaciales en GeoJSON GeoJSON geoprostorni podaci GeoJSON téradatok Data geospasial GeoJSON Dati geo-spaziali GeoJSON GeoJSON 지리 정보 데이터 Dane geoprzestrzenne GeoJSON Dados geoespaciais GeoJSON геопространственные данные GeoJSON Geopriestorové údaje GeoJSON ГеоЈСОН геопросторни подаци GeoJSON geospatial data GeoJSON coğrafi veriler геопросторові дані GeoJSON GeoJSON 地理空间数据 GeoJSON 地理空間資料 GPX geographic data dades geogràfiques GPX geografická data GPX GPX geographische Daten GPX geographic data datos geográficos en GPX GPX datu geografikoak GPX-paikkatieto GPX geografski podaci GPX földrajzi adatok Data geografis GPX Dati geografici GPX GPX 지리 공간정보 데이터 Donadas geograficas GPX Dane geograficzne GPX Dados geográficos GPX географические данные GPX Zemepisné údaje GPX ГПИкс географски подаци GPX geografisk data GPX coğrafi verileri географічні дані GPX GPX 地理空间数据 GPX 地理資料 GPX GPS Exchange Format Citrix ICA settings file ملف إعدادات Citrix ICA Fajł naładaŭ Citrix ICA Настройки — Citrix ICA fitxer d'ajusts de Citrix ICA soubor nastavení Citrix ICA Citrix ICA-opsætningsfil Citrix-ICA-Einstellungsdatei Αρχείο ρυθμίσεων Citrix ICA Citrix ICA settings file archivo de configuración de Citrix ICA Citrix ICA ezarpenen fitxategia Citrix ICA -asetustiedosto Citrix ICA stillingarfíla fichier de paramètres ICA Citrix comhad socruithe Citrix ICA ficheiro de configuracións de Citrix ICA קובץ הגדרות של Citrix ICA Citrix ICA datoteka postavki Citrix ICA beállításfájl File de configuration ICA Citrix Berkas penataan Citrix ICA File impostazioni Citrix ICA Citrix ICA 設定ファイル Citrix ICA-ის პარამეტრების ფაილი Citrix ICA баптаулар файлы 시트릭스 ICA 설정 파일 Citrix ICA parametrų failas Citrix ICA iestatījumu datne Innstillingsfil for Citrix ICA Citrix ICA-instellingen Citrix ICA-innstillingsfil fichièr de paramètres ICA Citrix Plik ustawień Citrix ICA ficheiro de definições Citrix ICA Arquivo de configuração do Citrix ICA Fișier de configurări Citrix ICA файл настроек Citrix ICA Súbor nastavení Citrix ICA Nastavitvena datoteka Citrix ICA File rregullimesh Citrix ICA датотека подешавања Цитрикс ИЦА-а Citrix ICA-inställningsfil Citrix ICA ayar dosyası файл параметрів ICA Citrix Tập tin thiết lập ICA Citrix Citrix ICA 设置文件 Citrix ICA 設定值檔案 ICA Independent Computing Architecture XUL interface document مستند واجهة XUL Interfejsny dakument XUL Документ — интерфейс за XUL document d'interfície XUL dokument rozhraní XUL XUL-grænsefladedokument XUL-Oberflächendokument Έγγραφο διεπαφής XUL XUL interface document documento de interfaz XUL XUL interfazearen dokumentua XUL-käyttöliittymäasiakirja XUL markamótsskjal document d'interface XUL cáipéis chomhéadan XUL documento de interface XUL מסמך ממשק XUL XUL dokument sučelja XUL-felületdokumentum Documento de interfacie XUL Dokumen antarmuka XUL Documento interfaccia XUL XUL インターフェイスドキュメント XUL интерфейс құжаты XUL 인터페이스 문서 XUL sąsajos dokumentas XUL saskarnes dokuments XUL-grensesnittdokument XUL-interface-document XUL-grensesnitt-dokument document d'interfàcia XUL Dokument interfejsu XUL documento de ambiente XUL Documento de interface XUL Document interfață XUL документ интерфейса XUL Dokument rozhrania XUL Dokument vmesnika XUL Dokument interfaqe XUL документ ИксУЛ сучеља XUL-gränssnittsdokument XUL arayüz belgesi документ інтерфейсу XUL Tài liệu giao diện XUL XUL 界面文档 XUL 介面文件 XUL XML User interface markup Language XPInstall installer module وحدة مثبت XPInstall Пакет — инсталация XPInstall mòdul de l'instal·lador XPinstall modul instalátoru XPInstall XPInstall-installationsmodul XPInstall-Installationsmodul Άρθρωμα εγκατάστασης XPInstall XPInstall installer module módulo del instalador XPInstall XPInstall instalatzailearen modulua XPInstall-asennuspaketti XPInstall innleggjaramótul module d'installation XPInstall modúl suiteála XPInstall Módulo do instalador XPInstall מודול התקנה של XPInstall XPInstall instalacijski modul XPInstall telepítőmodul Modulo de installation XPInstall Modul installer XPInstall Modulo installatore XPInstall XPInstall インストーラモジュール XPInstall орнату модулі XPInstall 설치 프로그램 모듈 XPInstall įdiegiklio modulis XPInstall instalatora modulis XPInstall installeer module modul d'installacion XPInstall Moduł instalatora XPInstall módulo de instalador XPInstall Módulo de instalador XPInstall Modul de instalare XPInstall модуль установщика XPInstall Modul inštalátora XPInstall modul namestilnika XPInstall модул инсталатера Инсталирања ИксПе-а XPInstall-installeringsmodul XPInstall kurulum modülü модуль засобу встановлення XPInstall XPInstall 安装工具模块 XPInstall 安裝程式模組 Word 2007 document مستند Word 2007 Документ — Word 2007 document de Word 2007 dokument Word 2007 Word 2007-dokument Word-2007-Dokument Έγγραφο Word 2007 Word 2007 document documento de Word 2007 Word 2007 dokumentua Word 2007 -asiakirja Word 2007 skjal document Word 2007 cáipéis Word 2007 documento de Word 2007 מסמך Word 2007 Word 2007 dokument Word 2007 dokumentum Documento Word 2007 Dokumen Word 2007 Documento Word 2007 Word 2007 ドキュメント Word 2007 құжаты Word 2007 문서 Word 2007 dokumentas Word 2007 dokuments Word 2007-document document Word 2007 Dokument Word 2007 documento Word 2007 Documento do Word 2007 Document Word 2007 документ Word 2007 Dokument Word 2007 Dokument Word 2007 документ Ворда 2007 Word 2007-dokument Word 2007 belgesi документ Word 2007 Tài liệu Word 2007 Microsoft Word 2007 文档 Word 2007 文件 Word 2007 document template Шаблон за документи — Word 2007 plantilla de document de Word 2007 šablona dokumentu Word 2007 Word 2007-dokumentskabelon Word-2007-Dokumentvorlage Πρότυπο έγγραφο Word 2007 Word 2007 document template plantilla de documento de Word 2007 Word 2007 dokumentuaren txantiloia Word 2007 -asiakirjamalli modèle de document Word 2007 Plantilla de documento de Word 2007 תבנית מסמך של Word 2007 Word 2007 predložak dokumenta Word 2007 dokumentumsablon Patrono de documento Word 2007 Templat dokumen Word 2007 Modello documento Word 2007 Word 2007 文書テンプレート Word 2007-ის დოკუმენტის შაბლონი Word 2007 құжатының үлгісі Word 2007 문서 서식 Word 2007 dokumenta veidne Word 2007 document sjabloon modèl de document Word 2007 Szablon dokumentu Word 2007 modelo de documento Word 2007 Modelo de documento do Word 2007 шаблон документа Word 2007 Šablóna dokumentu Word 2007 Predloga dokumenta Word 2007 шаблон документа Ворда 2007 Word 2007-dokumentmall Word 2007 belge şablonu шаблон документа Word 2007 Word 2007 文档模板 Word 2007 文件範本 PowerPoint 2007 presentation عرض تقديمي PowerPoint 2007 Презентация — PowerPoint 2007 presentació de PowerPoint 2007 prezentace PowerPoint 2007 PowerPoint 2007-præsentation PowerPoint-2007-Präsentation Παρουσίαση PowerPoint 2007 PowerPoint 2007 presentation presentación de PowerPoint 2007 PowerPoint 2007 aurkezpena PowerPoint 2007 -esitys PowerPoint 2007 framløga présentation PowerPoint 2007 láithreoireacht PowerPoint 2007 presentación de PowerPoint 2007 מצגת של PowerPoint 2007 PowerPoint 2007 prezentacija PowerPoint 2007 prezentáció Presentation PowerPoint 2007 Presentasi PowerPoint 2007 Presentazione standard PowerPoint 2007 PowerPoint 2007 プレゼンテーション PowerPoint 2007 презентациясы PowerPoint 2007 프레젠테이션 PowerPoint 2007 pateiktis PowerPoint 2007 prezentācija PowerPoint 2007-presentatie presentacion PowerPoint 2007 Prezentacja PowerPoint 2007 apresentação PowerPoint 2007 Apresentação do PowerPoint 2007 Prezentare PowerPoint 2007 презентация PowerPoint 2007 Prezentácia PowerPoint 2007 Predstavitev Microsoft PowerPoint 2007 презентација Пауер Поинта 2007 PowerPoint 2007-presentation PowerPoint 2007 sunumu презентація PowerPoint 2007 Trình diễn PowerPoint 2007 Microsoft PowerPoint 2007 演示文稿 PowerPoint 2007 簡報 PowerPoint 2007 slide Кадър — PoerPoint 2007 dispositiva de PowerPoint 2007 snímek PowerPoint 2007 PowerPoint 2007-slide PowerPoint 2007-Folie Διαφάνεια PowerPoint 2007 PowerPoint 2007 slide diapositiva de PowerPoint 2007 PowerPoint 2007 diapositiba PowerPoint 2007 -dia diapositive PowerPoint 2007 Diaporama de PowerPoint 2007 שקופית של PowerPoint 2007 PowerPoint 2007 slajd PowerPoint 2007 dia Diapositiva PowerPoint 2007 Slide PowerPoint 2007 Diapositiva PowerPoint 2007 PowerPoint 2007 スライド PowerPoint 2007-ის სლაიდი PowerPoint 2007 слайды PowerPoint 2007 슬라이드 PowerPoint 2007 slaids PowerPoint 2007 dia diapositive PowerPoint 2007 Slajd PowerPoint 2007 diapositivo PowerPoint 2007 Slide do PowerPoint 2007 слайд PowerPoint 2007 Snímka PowerPoint 2007 Prosojnica PowerPoint 2007 слајд Пауер Поинта 2007 PowerPoint 2007-bildspel PowerPoint 2007 slaytı слайд PowerPoint 2007 PowerPoint 2007 文稿 PowerPoint 2007 投影片 PowerPoint 2007 show عرض PowerPoint 2007 Презентация-шоу — PowerPoint 2007 exposició de PowerPoint 2007 prezentace PowerPoint 2007 PowerPoint 2007-dias PowerPoint-2007-Präsentation Παρουσίαση PowerPoint 2007 PowerPoint 2007 show presentación autoejecutable de PowerPoint 2007 PowerPoint 2007 ikuskizuna PowerPoint 2007 -diaesitys PowerPoint 2007 framsýning diaporama PowerPoint 2007 taispeántas PowerPoint 2007 Exposición de PowerPoint 2007 תצוגה של PowerPoint 2007 PowerPoint 2007 prezentacija PowerPoint 2007 bemutató Projection de diapositivas PowerPoint 2007 Presentasi PowerPoint 2007 Solo presentazione PowerPoint 2007 PowerPoint 2007 プレゼンテーション PowerPoint 2007 көрсетілімі PowerPoint 2007 쇼 PowerPoint 2007 pateiktis PowerPoint 2007 slīdrāde PowerPoint 2007 show diaporama PowerPoint 2007 Pokaz PowerPoint 2007 espetáculo PowerPoint 2007 Apresentação do PowerPoint 2007 Prezentare PowerPoint 2007 презентация PowerPoint 2007 Ukážka PowerPoint 2007 Zagonska predstavitev PowerPoint 2007 приказ Пауер Поинта 2007 PowerPoint 2007-visning PowerPoint 2007 gösterisi показ слайдів PowerPoint 2007 Microsoft PowerPoint 2007 演示文稿 PowerPoint 2007 展示 PowerPoint 2007 presentation template Шаблон за презентации — PowerPoint 2007 plantilla de presentació de PowerPoint 2007 šablona prezentace PowerPoint 2007 PowerPoint 2007-præsentationsskabelon PowerPoint 2007-Präsentationsvorlage Πρότυπο παρουσίασης PowerPoint 2007 PowerPoint 2007 presentation template plantilla de presentación de PowerPoint 2007 PowerPoint 2007 aurkezpen txantiloia PowerPoint 2007 -esitysmalli modèle de présentation PowerPoint 2007 modelo de presentación de PowerPoint 2007 תבנית למצגת של PowerPoint 2007 PowerPoint 2007 predložak prezentacije PowerPoint 2007 bemutatósablon Patrono de presentation PowerPoint 2007 Templat presentasi PowerPoint 2007 Modello presentazione PowerPoint 2007 PowerPoint 2007 プレゼンテーションテンプレート PowerPoint 2007-ის პრეზენტაციის შაბლონი PowerPoint 2007 презентация шаблоны PowerPoint 2007 프레젠테이션 서식 PowerPoint 2007 prezentācijas veidne PowerPoint 2007 presentation sjabloon modèl de presentacion PowerPoint 2007 Szablon prezentacji PowerPoint 2007 modelo de apresentação PowerPoint 2007 Modelo de apresentação do PowerPoint 2007 шаблон презентации PowerPoint 2007 Šablóna prezentácie PowerPoint 2007 Predloga predstavitve PowerPoint 2007 шаблон презентације Пауер Поинта 2007 PowerPoint 2007-presentationsmall PowerPoint 2007 sunum şablonu шаблон презентації PowerPoint 2007 PowerPoint 2007 演示文稿模板 PowerPoint 2007 簡報範本 Excel 2007 spreadsheet جدول Excel 2007 Таблица — Excel 2007 full de càlcul d'Excel 2007 sešit Excel 2007 Excel 2007-regneark Excel-2007-Tabelle Λογιστικό φύλλο Excel 2007 Excel 2007 spreadsheet hoja de cálculo de Excel 2007 Excel 2007 kalkulu-orria Excel 2007 -taulukko Excel 2007 rokniark feuille de calcul Excel 2007 scarbhileog Excel 2007 folla de cálculo de Excel 2007 גליון נתונים של אקסל 2007 Excel 2007 proračunska tablica Excel 2007 táblázat Folio de calculo Excel 2007 Lembar sebar Excel 2007 Foglio di calcolo Excel 2007 Excel 2007 スプレッドシート Excel 2007-ის ცხრილი Excel 2007 электрондық кестесі Excel 2007 스프레드시트 Excel 2007 skaičialentė Excel 2007 izklājlapa Excel 2007-rekenblad fuèlh de calcul Excel 2007 Arkusz Excel 2007 folha de cálculo Excel 2007 Planilha do Excel 2007 Foaie de calcul Excel 2007 электронная таблица Excel 2007 Zošit Excel 2007 Razpredelnica Microsoft Excel 2007 табела Ексела 2007 Excel 2007-kalkylblad Excel 2007 çalışma sayfası ел. таблиця Excel 2007 Bảng tính Excel 2007 Microsoft Excel 2007 工作簿 Excel 2007 試算表 Excel 2007 spreadsheet template Шаблон за таблици — Excel 2007 plantilla de full de càlcul d'Excel 2007 šablona sešitu Excel 2007 Excel 2007-regnearksskabelon Excel 2007-Tabellenvorlage Πρότυπο λογιστικού φύλλου Excel 2007 Excel 2007 spreadsheet template plantilla de hoja de cálculo de Excel 2007 Excel 2007 kalkulu-orri txantiloia Excel 2007 -taulukkomalli modèle de feuille de calcul Excel 2007 modelo de folla de cálculo Excel 2007 תבנית של גיליון נתונים של Excel 2007 Excel 2007 predložak proračunske tablice Excel 2007 táblázatsablon Patrono de folio de calculo Excel 2007 Templat lembar kerja Excel 2007 Modello foglio di calcolo Excel 2007 Excel 2007 スプレッドシートテンプレート Excel 2007-ის ცხრილის შაბლონი Excel 2007 кесте шаблоны Excel 2007 스프레드시트 서식 Excel 2007 izklājlapas veidne Excel 2007 spreadsheet sjabloon modèl de fuèlh de calcul Excel 2007 Szablon arkusza Excel 2007 modelo de folha de cálculo Excel 2007 Modelo de planilha do Excel 2007 шаблон электронной таблицы Excel 2007 Šablóna zošitu Excel 2007 Predloga razpredelnice Excel 2007 шаблон табеле Ексела 2007 Excel 2007-kalkylarksmall Excel 2007 çalışma sayfası şablonu шаблон електронної таблиці Excel 2007 Excel 2007 工作表模板 Excel 2007 試算表範本 T602 document مستند T602 Dakument T602 Документ — T602 document T602 dokument T602 T602-dokument T602-Dokument Έγγραφο T602 T602 document T602-dokumento documento T602 T602 dokumentua T602-asiakirja T602 skjal document T602 cáipéis T602 documento T602 מסמך T602 T602 dokument T602 dokumentum Documento T602 Dokumen T602 Documento T602 T602 ドキュメント T602 құжаты T602 문서 T602 dokumentas T602 dokuments T602-dokument T602-document T602-dokument document T602 Dokument T602 documento T602 Documento T602 Document T602 документ T602 Dokument T602 Dokument T602 Dokument T602 Т602 документ T602-dokument T602 belgesi документ T602 Tài liệu T602 T602 文档 T602 文件 Cisco VPN Settings إعدادات Cisco VPN Nałady Cisco VPN Настройки — ВЧМ на Cisco ajusts VPN de Cisco nastavení Cisco VPN Cisco VPN-opsætning Cisco-VPN-Einstellungen Ρυθμίσεις Cisco VPN Cisco VPN Settings configuración de VPN de Cisco Cisco VPN ezarpenak Cisco VPN -asetukset Cisco VPN stillingar paramètres VPN Cisco socruithe VPN Cisco configuracións de VPN de Cisco הגדרות של Cisco VPN Cisco VPN postavke Cisco VPN beállítások Configuration VPN Cisco Penataan Cisco VPN Impostazioni VPN Cisco Cisco VPN 設定 Cisco VPN-ის პარამეტრები Cisco VPN баптаулары Cisco VPN 설정 Cisco VPN parametrai Cisco VPN iestatījumi Cisco VPN-innstillinger Cisco VPN-instellingen Cisco VPN-innstillingar paramètres VPN Cisco Ustawienia VPN Cisco definições de Cisco VPN Configurações de VPN da Cisco Configurări VPN Cisco файл настроек Cisco VPN Nastavenia Cisco VPN Datoteka nastavitev Cisco VPN Rregullime VPN Cisco подешавања Циско ВПН-а Cisco VPN-inställningar Cisco VPN Ayarları параметри VPN Cisco Thiết lập VPN Cisco Cisco VPN 设置 Cisco VPN 設定值 ICC profile تشكيلة OCL Цветови профил — OCL perfil ICC profil ICC ICC-profil ICC-Profil Προφίλ ICC ICC profile ICC-profilo perfil ICC ICC profila ICC-profiili ICC umhvarv profil ICC próifíl ICC perfíl ICC פרופיל ICC ICC profil ICC profil Profilo ICC Profil ICC Profilo ICC ICC プロファイル ICC профайлы ICC 프로필 ICC profilis ICC profils ICC profiel perfil ICC Profil ICC perfil ICC Perfil ICC Profil ICC профиль ICC Profil farieb ICC Datoteka profila ICC ИЦЦ профил ICC-profil ICC profili профіль ICC ICC 文件 ICC 設定檔 IT 8.7 color calibration file ملف ضبط ألوان IT 8.7 Файл за цветово калибриране — IT 8.7 fitxer de calibratge de color IT 8.7 soubor kalibrace barev IT 8.7 IT 8.7 farvekalibreringsfil IT 8.7-Farbkalibrierungsdatei Αρχείο βαθμονόμησης χρώματος ΙΤ 8.7 IT 8.7 color calibration file archivo de calibración de color IT 8.7 IT 8.7 kolore-kalibrazioaren fitxategia IT 8.7 -värikalibrointitiedosto IT 8.7 litstillingarfíla fichier de calibration couleur IT 8.7 comhad calabraithe dathanna IT 8.7 ficheiro de calibración de cor IT 8.7 קובץ כיול צבע IT 8.7 IT 8.7 datoteka kalibracije boja IT 8.7 színkalibrációs fájl File de calibration de colores IT 8.7 Berkas kalibrasi warna IT 8.7 File calibrazione colore IT 8.7 IT 8.7 カラーキャリブレーションファイル IT 8.7 түс баптау файлы IT 8.7 색 조율 파일 IT 8.7 spalvų kalibravimo failas IT 8.7 krāsu kalibrācijas datne IT 8.7 kleurcalibratie bestand fichièr de calibracion color IT 8.7 Plik kalibracji kolorów IT 8.7 ficheiro de calibração de cor IT 8.7 Arquivo de calibração de cor IT 8.7 Fișier de calibrare a culorii IT 8.7 файл калибровки цвета IT 8.7 Súbor kalibrácie farieb IT 8.7 Umeritvena datoteka barve IT 8.7 ИТ 8.7 датотека калибрације боје IT 8.7-färgkalibreringsfil IT 8.7 renk kalibrasyon dosyası файл калібрування кольорів IT 8.7 IT 8.7 色彩校准文件 IT 8.7 色彩校正檔 CCMX color correction file fitxer de correcció de color CCMX soubor korekce barev CCMX CCMX-farvekorrektionsfil CCMX-Farbkorrekturdatei Αρχείο διόρθωσης χρωμάτων CCMX CCMX colour correction file archivo de corrección de color CCMX CCMX kolore-kalibrazioaren fitxategia CCMX-värikorjaustiedosto fichier de correction colorimétrique CCMX Ficheiro de corrección de cor CCMX קובץ תיקון צבע מסוג CCMX CCMX datotkea ispravka boja CCMX színjavítási fájl File de correction de colores CCMX Berkas koreksi warna CCMX File correzione colore CCMX CCMX カラー訂正ファイル CCMX түсті келтіру файлы CCMX 색상 보정 파일 CCMX krāsu korekciju datne fichièr de correccion colorimetrica CCMX Plik korekcji kolorów CCMX ficheiro de correção de cor CCMX Arquivo de correção de cor CCMX файл цветовой коррекции CCMX Súbor korekcie farieb CCMX Datoteka barvne poprave CCMX ЦЦМИкс датотека поправке боје CCMX-färgkorrigeringsfil CCMX renk düzeltme dosyası файл даних виправлення кольорів CCMX CCMX 色彩校准文件 CCMX 色彩校正檔 WinHelp help file fitxer d'ajuda WinHelp soubor nápovědy WinHelp WinHelp-hjælpefil WinHelp-Hilfedatei Αρχείο βοήθειας WinHelp WinHelp help file archivo de ayuda de WinHelp WinHelp laguntza fitxategia WinHelp-ohjetiedosto fichier d'aide WinHelp Ficheiro de axuda WinHelp קובץ עזרה מסוג WinHelp WinHelp datoteka pomoći WinHelp súgófájl File de adjuta WinHelp Berkas bantuan WinHelp File aiuto WInHelp WinHelp ヘルプファイル WinHelp көмек файлы WinHelp 도움말 파일 WinHelp palīdzības datne fichièr d'ajuda WinHelp Plik pomocy WinHelp ficheiro de ajuda WinHelp Arquivo de ajuda WinHelp файл справки WinHelp Súbor Pomocníka WinHelp Datoteka pomoči WinHelp датотека помоћи Вин хелпа WinHelp-hjälpfil WinHelp yardım dosyası файл довідки WinHelp WinHelp 帮助文件 WinHelp 說明檔 binary differences between files digital photos الصور الرقمية ličbavyja zdymki Цифрови фотографии fotos digitals digitální fotografie digitale billeder Digitale Fotos Ψηφιακές φωτογραφίες digital photos fotos digitales argazki digitalak digivalokuvia talgildar myndir photos numériques grianghraif dhigiteacha fotos dixitais תמונות דיגיטליות digitalne fotografije digitális fényképek Photos digital foto digital Foto digitali デジタルフォト сандық фотосуреттер 디지털 사진 skaitmeninės nuotraukos digitāla fotogrāfija digitale foto's digitale fotografi fòtos numericas Zdjęcia cyfrowe fotografias digitais Fotos digitais fotografii digitale цифровые фотографии Digitálne fotografie digitalne fotografije Fotografi dixhitale дигиталне фотографије digitalbilder sayısal fotoğraflar цифрові фотографії ảnh chụp số 数字化图像 數位相片 Video CD Video CD Videa CD CD — видео Video CD Video CD Video-cd Video-CD Video CD Video CD Video-KD Video CD Bideo CDa Video CD Video CD CD vidéo Video CD Video CD תקליטור וידאו Video CD Video CD Video CD Video CD Video CD ビデオ CD видео CD 비디오 CD Vaizdo CD Video CD video-CD Video-CD CD vidèo Video CD Video CD CD de vídeo CD video видеодиск VCD Video CD Video CD CD Video Видео ЦД Video-cd Video CD Video CD Đĩa CD ảnh động VCD Video CD Super Video CD Super Video CD Super Video CD CD — супер видео Super Video CD Super Video CD Super Video-cd Super-Video-CD Super Video CD Super Video CD Super-Video-KD Super Video CD Super Bideo CDa Super Video CD Super Video CD Super VCD Super Video CD Super vídeo CD Super Video CD Super Video CD Super Video CD Super Video CD Super Video CD Super Video CD スーパービデオ CD Super Video CD 수퍼 비디오 CD Super vaizdo CD Super Video CD super-video-CD Super Video-CD Super VCD Super Video CD Super Video CD CD de Super Vídeo (SVCD) Super Video CD компакт-диск Super Video Super Video CD Super Video CD CD Super Video Супер видео ЦД Super Video CD Super Video CD Super Video CD Đĩa CD siêu ảnh động SVCD Super Video CD video DVD DVD مرئي videa DVD DVD — видео DVD-Video videodisk DVD video-dvd Video-DVD Βίντεο DVD video DVD video-DVD DVD de vídeo bideo DVDa video-DVD video DVD DVD vidéo DVD físe DVD de vídeo DVD וידאו video DVD video DVD DVD video DVD video DVD video ビデオ DVD ვიდეო DVD видео DVD 동영상 DVD vaizdo DVD video DVD video-DVD Video-DVD DVD vidèo DVD-Video DVD vídeo DVD de vídeo DVD video видео-DVD DVD-Video video DVD DVD video видео ДВД video-dvd video DVD відео-DVD đĩa DVD ảnh động 视频 DVD 視訊 DVD audio CD CD سمعي aŭdyjo CD CD — аудио CD d'àudio zvukové CD lyd-cd Audio-CD CD ήχου audio CD Son-KD CD de sonido Audio CDa ääni-CD audio CD CD audio dlúthdhiosca fuaime CD de son תקליטור שמע Glazbeni CD hang CD CD audio CD audio CD audio オーディオ CD аудио CD 오디오 CD garso CD audio CD audio-CD lyd-CD CD àudio CD-Audio CD áudio CD de áudio CD audio звуковой CD Zvukové CD zvočni CD CD audio звучни ЦД ljud-cd Müzik CD'si звуковий CD đĩa CD âm thanh 音频 CD 音訊 CD blank CD disc قرص CD فارغ čysty dysk CD CD — празно disc CD en blanc prázdný disk CD tom cd-disk Leere CD Κενό CD blank CD disc disco CD en blanco CD disko hutsa tyhjä CD-levy blonk fløga CD vierge dlúthdhiosca folamh disco de CD en brancho תקליטור ריק Prazni CD disk üres CD-lemez Disco CD vacue cakram CD kosong Disco vuoto CD ブランク CD ディスク таза CD дискі 빈 CD 디스크 tuščias CD diskas tukšs CD disks blanco CD tom CD-plate CD verge Pusta płyta CD CD vazio Disco CD vazio disc gol CD чистый компакт-диск Prázdny disk CD prazen CD disk Disk bosh CD празан ЦД диск tom cd-skiva boş CD diski порожній компакт-диск đĩa CD trống 空 CD 光盘 空白 CD 光碟 blank DVD disc قرص DVD فارغ čysty dysk DVD DVD — празно disc DVD en blanc prázdný disk DVD tom dvd-disk Leere DVD Κενό DVD blank DVD disc disco DVD en blanco DVD disko hutsa tyhjä DVD-levy blonk margfløga DVD vierge DVD folamh disco de DVD en branco תקליטור DVD ריק Prazni DVD disk üres DVD-lemez Disco DVD vacue cakram DVD kosong Disco vuoto DVD ブランク DVD ディスク таза DVD дискі 빈 DVD 디스크 tuščias DVD diskas tukšs DVD disks blanco DVD tom DVD-plate DVD verge Pusta płyta DVD DVD vazio Disco DVD vazio disc gol DVD чистый диск DVD Prázdny disk DVD prazen DVD disk Disk bosh DVD празан ДВД диск tom dvd-skiva boş DVD diski порожній диск DVD đĩa DVD trống 空 DVD 光盘 空白 DVD 光碟 blank Blu-ray disc قرص بلو-راي فارغ čysty dysk Blu-ray Blu-ray — празно disc Blu-Ray en blanc prázdný disk Blu-ray tom Blu-ray-disk Leere Blu-ray-Scheibe Κενό Blu-ray blank Blu-ray disc disco Blu-ray en blanco Blu-ray disko hutsa tyhjä Blu-ray-levy blankur Blu-ray diskur disque Blu-Ray vierge diosca folamh Blu-Ray disco Blu-ray en branco תקליטור בלו־ריי ריק Prazni Blu-ray disk üres Blu-Ray lemez Disco Bly-ray vacue cakram Blu-ray kosong Disco vuoto Blu-ray ブランク Blu-ray ディスク таза Blu-ray дискі 빈 블루레이 디스크 tuščias Blu-ray diskas tukšs Blu-ray disks blanco Blu-ray-disk tom Blu-Ray-plate disc Blu-Ray verge Pusta płyta Blu-ray Blu-Ray vazio Disco Blu-ray vazio disc gol Blu-ray чистый диск Blu-ray Prázdny disk Blu-ray prazen Blu-Ray disk Disk bosh Blu-ray празан Блу-реј диск tom Blu-ray-skiva boş Blue-ray diski порожній диск Blu-ray đĩa Blu-ray trống 空蓝光 DVD 空白 Blu-ray 光碟 blank HD DVD disc قرص HD DVD فارغ čysty dysk HD DVD HD DVD — празно disc HD-DVD en blanc prázdný disk HD DVD tom HD dvd-disk Leere HD-DVD Κενό HD DVD blank HD DVD disc disco HD DVD en blanco HD DVD disko hutsa tyhjä HD DVD -levy blankur HD DVD diskur disque HD-DVD vierge HD DVD folamh disco de HD DVD en branco דיסק HD DVD ריק Prazni HD DVD disk üres HD DVD-lemez Disco HD DVD vacue cakram HD DVD kosong Disco vuoto DVD HD ブランク HD DVD ディスク таза HD DVD дискі 빈 HD DVD 디스크 tuščias HD DVD diskas tukšs HD DVD disks blanco HD-DVD tom HD-DVD-plate disc HD-DVD verge Pusta płyta HD DVD HD DVD vazio Disco HD DVD vazio disc gol HD DVD чистый диск HD DVD Prázdny disk HD DVD prazen HD DVD disk Disk bosh DVD HD празан ХД ДВД диск tom HD DVD-skiva boş HD DVD diski порожній диск HD DVD đĩa DVD HD trống 空 HD DVD 光盘 空白 HD DVD 光碟 audio DVD DVD سمعي aŭdyjo DVD DVD — аудио DVD d'àudio zvukové DVD lyd-dvd Audio-DVD DVD ήχου audio DVD Son-DVD DVD de sonido audio DVDa ääni-DVD Ljóð DVD DVD audio DVD fuaime DVD de son DVD שמע Glazbeni DVD hang DVD DVD audio DVD audio DVD audio オーディオ DVD аудио DVD 오디오 DVD garso DVD audio DVD audio-DVD lyd-DVD DVD àudio DVD-Audio DVD áudio DVD de áudio DVD audio звуковой DVD Zvukové DVD zvočni DVD DVD audio звучни ДВД ljud-dvd Müzik DVD'si звуковий DVD đĩa DVD âm thanh 音频 DVD 音訊 DVD Blu-ray video disc قرص بلو-راي مرئي Videadysk Blu-ray Blu-ray — видео disc de vídeo Blu-Ray videodisk Blu-ray Blu-ray video-disk Blu-ray-Videoscheibe Δίσκος βίντεο Blu-ray Blu-ray video disc disco de vídeo Blu-ray Blu-ray bideo-diskoa Blu-ray-videolevy Blu-ray diskur disque vidéo Blu-Ray diosca físe Blu-Ray disco de vídeo Blu-ray תקליטור וידאו מסוג בלו־ריי Blu-ray video disk Blu-ray videolemez Disco video Blu-ray Cakram video Blu-ray Disco video Blu-ray Blu-ray ビデオディスク Blu-ray ვიდეო დისკი Blu-ray видео дискі 블루레이 동영상 디스크 Blu-ray vaizdo diskas Blu-ray video disks Blu-ray-videodisk Blu-Ray videoplate disc vidèo Blu-Ray Płyta wideo Blu-ray Blu-ray de vídeo Disco de vídeo Blu-ray Disc video Blu-ray видеодиск Blu-ray Videodisk Blu-ray Blu-ray video disk Disk video Blu-ray Блу-реј видео диск Blu-ray-videoskiva Blu-ray video diski відеодиск Blu-ray Đĩa ảnh động Blu-ray 蓝光视频光盘 Blu-ray 視訊光碟 HD DVD video disc قرص HD DVD مرئي Videadysk HD DVD HD DVD — видео disc de vídeo HD-DVD Videodisk HD DVD HD DVD-videodisk HD-DVD-Videoscheibe Δίσκος βίντεο HD DVD HD DVD video disc disco de vídeo HD DVD HD DVD bideo-diskoa HD DVD -videolevy HD DVD video diskur disque vidéo HD DVD diosca físe HD DVD disco de vídeo HD DVD תקליטור וידאו HD DVD HD DVD video disk HD DVD videolemez Disco video HD DVD Cakram video HD DVD Disco video DVD HD HD DVD ビデオディスク HD DVD видео дискі HD DVD 동영상 디스크 HD DVD vaizdo diskas HD DVD video disks HD-DVD-videodisk HD-DVD-videodisk disc vidèo HD DVD Płyta wideo HD DVD HD DVD de vídeo Disco de vídeo HD DVD Disc video HD DVD видеодиск HD DVD Videodisk HD DVD HD DVD video disk Disk video DVD HD ХД ДВД видео диск HD DVD-videoskiva HD DVD vidyo diski відеодиск HD DVD Đĩa ảnh động DVD HD HD DVD 视频光盘 HD DVD 視訊光碟 e-book reader Четец на е-книги lector de llibres electrònics čtečka elektronických knih e-bogslæser E-Book-Leser Αναγνώστης ηλεκτρονικών βιβλίων e-book reader lector de libros electrónicos e-book irakurlea e-kirjan lukulaite lecteur de livre numérique lector de libros electrónicos קורא ספרים אלקטרוניים čitač e-knjiga e-könyvolvasó Lector de libro electronic Pembaca e-book Lettore e-book 電子書籍リーダー электронды кітаптарды оқу құрылғысы 전자책 리더 e-grāmatu lasītājs e-book reader lector de libre numeric Czytnik e-booków leitor de ebooks Leitor de e-book устройство для чтения электронных книг Čítačka e-kníh Bralnik elektronskih knjig читач ел. књига e-bokläsare e-kitap okuyucu пристрій для читання електронних книг 电子书阅读器 e-book 閱讀器 Picture CD Picture CD Picture CD CD — изображения Picture CD Picture CD Billedcd Picture CD CD εικόνων Picture CD Picture CD Picture CD Picture CD Picture CD CD Picture Picture CD Picture CD תקליטור תמונות Slikovni CD Picture CD Disco Picture CD CD Gambar Picture CD ピクチャー CD Picture CD Picture CD Paveikslėlių CD Attēlu CD foto-CD Bilete-CD CD Picture Picture CD Picture CD CD de Fotos CD cu fotografii Picture CD Picture CD Slikovni CD Picture CD ЦД са сликама Picture CD Resim CD'si CD з зображеннями Đĩa CD ảnh 柯达 Picture CD 圖片 CD portable audio player مشغل الملفات المسموعة المحمولة pieranosny aŭdyjoplayer Преносим аудио плеър reproductor d'àudio portàtil přenosný zvukový přehrávač bærbar lydafspiller Portables Audio-Wiedergabegerät Φορητός αναπαραγωγέας μουσικής portable audio player dispositivo de sonido portátil audio erreproduzigailu eramangarria siirrettävä äänisoitin leysur ljóðavspælari lecteur audio portable seinnteoir iniompartha fuaime dispositivo de son portábel נגן מוזיקה נייד prenosivi audio svirač hordozható zenelejátszó Lector audio portabile pemutar audio portable Lettore audio portabile ポータブルオーディオプレイヤー тасымалы аудио плеер 휴대용 오디오 재생기 nešiojamasis garso leistuvas portatīvais audio atskaņotājs draagbare audiospeler portable audio layer lector àudio portable Przenośny odtwarzacz dźwięku reprodutor áudio portátil Reprodutor de áudio portátil player audio portabil портативный аудиопроигрыватель Prenosný hudobný prehrávač prenosni predvajalnik zvoka Lexues audio portativ преносна музичка справица bärbar ljudspelare taşınabilir ses oynatıcısı портативний аудіопрогравач bộ phát nhạc di động 便携式音频播放器 可攜式音訊播放程式 software برنامج prahrama Софтуер programari software software Software Λογισμικό software software softwarea ohjelmisto ritbúnaður logiciel bogearraí software תכנה softver szoftver Software peranti lunak Software ソフトウェア პროგრამული უზრუნველყოფა бағдарламалық қамтама 소프트웨어 programinė įranga programmatūra software programvare logicial Oprogramowanie programa Aplicativo software программное обеспечение Softvér programska oprema Software софтвер programvara yazılım програмне забезпечення phần mềm 软件 軟體 UNIX software برنامج يونكس Софтуер за UNIX programari d'UNIX software systému UNIX UNIX-programmer UNIX-Software Λογισμικό UNIX UNIX software software de UNIX UNIXeko softwarea UNIX-ohjelmisto UNIX ritbúnaður logiciel UNIX bogearraí UNIX Software de UNIX תכנה ל־UNIX UNIX softver UNIX-szoftver Software pro UNIX Peranti lunak UNIX Software UNIX UNIX ソフトウェア UNIX бағдарламасы UNIX 소프트웨어 UNIX programinė įranga UNIX programmatūra UNIX software logicial UNIX Oprogramowanie systemu UNIX programa UNIX Aplicativo UNIX Software UNIX программа UNIX Softvér UNIX Programska datoteka UNIX ЈУНИКС-ов софтвер UNIX-programvara UNIX yazılımı програмне забезпечення UNIX UNIX 软件 UNIX 軟體 Windows software برنامج ويندوز Софтуер — Windows programari de Windows software systému Windows Windowsprogram Windows-Software Λογισμικό Windows Windows software software de Windows Windows-eko softwarea Windows-ohjelmisto Windows ritbúnaður logiciel Windows bogearraí Windows Software de Windows תכנה ל־Windows Windows softver Windows-szoftver Software Windows Piranti lunak Windows Software Windows Windows ソフトウェア Windows бағдарламасы Windows 소프트웨어 Windows programinė įranga Windows programmatūra Windows software logicial Windows Oprogramowanie systemu Windows programa Windows Programa do Windows Software Windows программа Windows Softvér Windows Programska oprema za okolje Windows Виндоузов софтвер Windows-program Windows yazılımı програмне забезпечення Windows Windows 软件 Windows 軟體 TriG RDF document document TriG RDF dokument Trig RDF TriG RDF-dokument TriG-RDF-Dokument Έγγραφο TriG RDF TriG RDF document documento RDF de TriG TriG RDF dokumentua TriG RDF -asiakirja document RDF TriG Documento RDF TriG מסמך RDF של TriG TriG RDF dokument TriG RDF dokumentum Documento TriG RDF Dokumen TriG RDF Documento TriG RDF TriG RDF құжаты TriG RDF 문서 document RDF TriG Dokument RDF TriG documento TriG RDF Documento RDF do TriG Документ TriG RDF RDF dokument TriG Dokument TriG RDF ТриГ РДФ документ TriG RDF-dokument TriG RDF belgesi документ RDF TriG TriG RDF 文档 TriG RDF 文件 TriG TriG RDF Graph Triple Language Apple Keynote 5 presentation presentació Keynote 5 d'Apple prezentace Apple Keynote 5 Apple Keynote 5-præsentation Apple-Keynote-5-Präsentation Παρουσίαση Apple Keynote 5 Apple Keynote 5 presentation presentación de Apple Keynote 5 Apple Keynote 5 aurkezpena Apple Keynote 5 -esitys présentation Apple Keynote 5 Presentación de Apple Keynote 5 מצגת Apple Keynote 5 Apple Keynote 5 prezentacija Apple Keynote 5 prezentáció Presentation Apple Keynote 5 Presentasi Apple Keynote 5 Presentazione Apple Keynote 5 Apple Keynote 5 презентациясы Apple 키노트 5 프레젠테이션 presentacion Apple Keynote 5 Prezentacja Apple Keynote 5 apresentação Apple Keynote 5 Apresentação do Apple Keynote 5 Презентация Apple Keynote 5 Prezentácia Apple Keynote 5 Predstavitev Apple Keynote 5 презентација Епл Кинота 5 Apple Keynote 5-presentation Apple Keynote 5 sunumu презентація Apple Keynote 5 Apple Keynote 5 演示文稿 Apple Keynote 5 簡報 Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe 페이지메이커 Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Dokument Adobe PageMaker Адобе Пејџ Мејкер Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Adobe PageMaker Doom WAD WAD de Doom datový balík WAD hry Doom Doom WAD Doom WAD Doom WAD WAD de Doom Doom WAD Doom WAD Doom WAD WAD pro Doom WAD Doom WAD Doom Doom WAD 둠 WAD Plik WAD gry Doom Doom WAD Doom WAD WAD Doom Doom WAD Дум ВАД Doom-WAD Doom WAD WAD Doom Doom WAD Doom WAD WAD Where's All the Data Amiga disk image imatge de disc d'Amiga obraz disku pro Amigu Amiga-diskaftryk Amiga-Datenträgerabbild Εικόνα δίσκου Amiga Amiga disk image imagen de disco de Amiga Amiga disko irudia Amiga-levytiedosto Amiga slika diska Amiga lemezkép Imagine de disco Amiga Image disk Amiga Disco immagine Amiga Amiga диск бейнесі 아미가 디스크 이미지 imatge disc Amiga Obraz dysku Amiga imagem de disco Amiga Imagem de disco Amiga образ диска Amiga Obraz disku Amiga слика диска Амиге Amiga-diskavbild Amiga disk kalıbı образ диска Amiga Amiga 磁盘镜像 Amiga 磁碟映像檔 Flatpak application bundle Flatpak repository description Squashfs filesystem Snap package spectral/include/libQuotient/.clang-format0000644000175000000620000000746413566674122020674 0ustar dilingerstaff# Copyright (C) 2019 Project Quotient # # You may use this file under the terms of the LGPL-2.1 license # See the file LICENSE from this package for details. # This is the clang-format configuration style to be used by libQuotient. # Inspired by: # https://code.qt.io/cgit/qt/qt5.git/plain/_clang-format # https://wiki.qt.io/Qt_Coding_Style # https://wiki.qt.io/Coding_Conventions # Further information: https://clang.llvm.org/docs/ClangFormatStyleOptions.html # For convenience, the file includes commented out settings that we assume # to borrow from the WebKit style. The values for such settings try to but # are not guaranteed to coincide with the latest version of the WebKit style. --- Language: Cpp BasedOnStyle: WebKit #AccessModifierOffset: -4 AlignAfterOpenBracket: Align #AlignConsecutiveAssignments: false #AlignConsecutiveDeclarations: false AlignEscapedNewlines: Left AlignOperands: true #AlignTrailingComments: false #AllowAllParametersOfDeclarationOnNextLine: true #AllowShortBlocksOnASingleLine: false #AllowShortCaseLabelsOnASingleLine: false #AllowShortFunctionsOnASingleLine: All #AllowShortIfStatementsOnASingleLine: false #AllowShortLoopsOnASingleLine: false #AlwaysBreakAfterReturnType: None #AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: true #BinPackArguments: true #BinPackParameters: true BraceWrapping: AfterClass: false AfterControlStatement: false AfterEnum: false AfterFunction: true AfterNamespace: false AfterStruct: false AfterUnion: false AfterExternBlock: false BeforeCatch: false BeforeElse: false IndentBraces: false SplitEmptyFunction: false SplitEmptyRecord: false SplitEmptyNamespace: false BreakBeforeBinaryOperators: NonAssignment BreakBeforeBraces: Custom #BreakBeforeInheritanceComma: false #BreakInheritanceList: BeforeColon # Only supported since clang-format 7 #BreakBeforeTernaryOperators: true #BreakConstructorInitializersBeforeComma: false #BreakConstructorInitializers: BeforeComma #BreakStringLiterals: true ColumnLimit: 80 #CommentPragmas: '^!|^:' CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: true #ConstructorInitializerIndentWidth: 4 #ContinuationIndentWidth: 4 Cpp11BracedListStyle: false #DerivePointerAlignment: false FixNamespaceComments: true ForEachMacros: - foreach - Q_FOREACH - forever IncludeBlocks: Regroup IncludeCategories: - Regex: '^ * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "events/stateevent.h" #include namespace Quotient { class StateEventBase; class EventStatus { Q_GADGET public: /** Special marks an event can assume * * This is used to hint at a special status of some events in UI. * All values except Redacted and Hidden are mutually exclusive. */ enum Code { Normal = 0x0, //< No special designation Submitted = 0x01, //< The event has just been submitted for sending FileUploaded = 0x02, //< The file attached to the event has been // uploaded to the server Departed = 0x03, //< The event has left the client ReachedServer = 0x04, //< The server has received the event SendingFailed = 0x05, //< The server could not receive the event Redacted = 0x08, //< The event has been redacted Hidden = 0x10, //< The event should not be shown in the timeline }; Q_DECLARE_FLAGS(Status, Code) Q_FLAG(Status) }; class EventItemBase { public: explicit EventItemBase(RoomEventPtr&& e) : evt(std::move(e)) { Q_ASSERT(evt); } const RoomEvent* event() const { return rawPtr(evt); } const RoomEvent* get() const { return event(); } template const EventT* viewAs() const { return eventCast(evt); } const RoomEventPtr& operator->() const { return evt; } const RoomEvent& operator*() const { return *evt; } // Used for event redaction RoomEventPtr replaceEvent(RoomEventPtr&& other) { return std::exchange(evt, move(other)); } protected: template EventT* getAs() { return eventCast(evt); } private: RoomEventPtr evt; }; class TimelineItem : public EventItemBase { public: // For compatibility with Qt containers, even though we use // a std:: container now for the room timeline using index_t = int; TimelineItem(RoomEventPtr&& e, index_t number) : EventItemBase(std::move(e)), idx(number) {} index_t index() const { return idx; } private: index_t idx; }; template <> inline const StateEventBase* EventItemBase::viewAs() const { return evt->isStateEvent() ? weakPtrCast(evt) : nullptr; } template <> inline const CallEventBase* EventItemBase::viewAs() const { return evt->isCallEvent() ? weakPtrCast(evt) : nullptr; } class PendingEventItem : public EventItemBase { Q_GADGET public: using EventItemBase::EventItemBase; EventStatus::Code deliveryStatus() const { return _status; } QDateTime lastUpdated() const { return _lastUpdated; } QString annotation() const { return _annotation; } void setDeparted() { setStatus(EventStatus::Departed); } void setFileUploaded(const QUrl& remoteUrl); void setReachedServer(const QString& eventId) { setStatus(EventStatus::ReachedServer); (*this)->addId(eventId); } void setSendingFailed(QString errorText) { setStatus(EventStatus::SendingFailed); _annotation = std::move(errorText); } void resetStatus() { setStatus(EventStatus::Submitted); } private: EventStatus::Code _status = EventStatus::Submitted; QDateTime _lastUpdated = QDateTime::currentDateTimeUtc(); QString _annotation; void setStatus(EventStatus::Code status) { _status = status; _lastUpdated = QDateTime::currentDateTimeUtc(); _annotation.clear(); } }; inline QDebug& operator<<(QDebug& d, const TimelineItem& ti) { QDebugStateSaver dss(d); d.nospace() << "(" << ti.index() << "|" << ti->id() << ")"; return d; } } // namespace Quotient Q_DECLARE_METATYPE(Quotient::EventStatus) spectral/include/libQuotient/lib/networksettings.h0000644000175000000620000000317413566674122022504 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "settings.h" #include Q_DECLARE_METATYPE(QNetworkProxy::ProxyType) namespace Quotient { class NetworkSettings : public SettingsGroup { Q_OBJECT QTNT_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) QTNT_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) QTNT_DECLARE_SETTING(quint16, proxyPort, setProxyPort) Q_PROPERTY(QString proxyHost READ proxyHostName WRITE setProxyHostName) public: template explicit NetworkSettings(ArgTs... qsettingsArgs) : SettingsGroup(QStringLiteral("Network"), qsettingsArgs...) {} ~NetworkSettings() override = default; Q_INVOKABLE void setupApplicationProxy() const; }; } // namespace Quotient spectral/include/libQuotient/lib/converters.cpp0000644000175000000620000000312013566674122021746 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "converters.h" #include using namespace Quotient; QJsonValue JsonConverter::dump(const QVariant& v) { return QJsonValue::fromVariant(v); } QVariant JsonConverter::load(const QJsonValue& jv) { return jv.toVariant(); } QJsonObject JsonConverter::dump(const variant_map_t& map) { return #if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) QJsonObject::fromVariantHash #else QJsonObject::fromVariantMap #endif (map); } variant_map_t JsonConverter::load(const QJsonValue& jv) { return jv.toObject(). #if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) toVariantHash #else toVariantMap #endif (); } spectral/include/libQuotient/lib/networksettings.cpp0000644000175000000620000000270513566674122023036 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "networksettings.h" using namespace Quotient; void NetworkSettings::setupApplicationProxy() const { QNetworkProxy::setApplicationProxy( { proxyType(), proxyHostName(), proxyPort() }); } QTNT_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType) QTNT_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", {}, setProxyHostName) QTNT_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort) spectral/include/libQuotient/lib/networkaccessmanager.cpp0000644000175000000620000000452513566674122023774 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "networkaccessmanager.h" #include #include using namespace Quotient; class NetworkAccessManager::Private { public: QList ignoredSslErrors; }; NetworkAccessManager::NetworkAccessManager(QObject* parent) : QNetworkAccessManager(parent), d(std::make_unique()) {} QList NetworkAccessManager::ignoredSslErrors() const { return d->ignoredSslErrors; } void NetworkAccessManager::addIgnoredSslError(const QSslError& error) { d->ignoredSslErrors << error; } void NetworkAccessManager::clearIgnoredSslErrors() { d->ignoredSslErrors.clear(); } static NetworkAccessManager* createNam() { auto nam = new NetworkAccessManager(QCoreApplication::instance()); // See #109. Once Qt bearer management gets better, this workaround // should become unnecessary. nam->connect(nam, &QNetworkAccessManager::networkAccessibleChanged, [nam] { nam->setNetworkAccessible(QNetworkAccessManager::Accessible); }); return nam; } NetworkAccessManager* NetworkAccessManager::instance() { static auto* nam = createNam(); return nam; } NetworkAccessManager::~NetworkAccessManager() = default; QNetworkReply* NetworkAccessManager::createRequest( Operation op, const QNetworkRequest& request, QIODevice* outgoingData) { auto reply = QNetworkAccessManager::createRequest(op, request, outgoingData); reply->ignoreSslErrors(d->ignoredSslErrors); return reply; } spectral/include/libQuotient/lib/avatar.cpp0000644000175000000620000001554013566674122021043 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "avatar.h" #include "connection.h" #include "events/eventcontent.h" #include "jobs/mediathumbnailjob.h" #include #include #include #include #include using namespace Quotient; using std::move; class Avatar::Private { public: explicit Private(QUrl url = {}) : _url(move(url)) {} ~Private() { if (isJobRunning(_thumbnailRequest)) _thumbnailRequest->abandon(); if (isJobRunning(_uploadRequest)) _uploadRequest->abandon(); } QImage get(Connection* connection, QSize size, get_callback_t callback) const; bool upload(UploadContentJob* job, upload_callback_t callback); bool checkUrl(const QUrl& url) const; QString localFile() const; QUrl _url; // The below are related to image caching, hence mutable mutable QImage _originalImage; mutable std::vector> _scaledImages; mutable QSize _requestedSize; mutable enum { Unknown, Cache, Network, Banned } _imageSource = Unknown; mutable QPointer _thumbnailRequest = nullptr; mutable QPointer _uploadRequest = nullptr; mutable std::vector callbacks; }; Avatar::Avatar() : d(std::make_unique()) {} Avatar::Avatar(QUrl url) : d(std::make_unique(std::move(url))) {} Avatar::Avatar(Avatar&&) = default; Avatar::~Avatar() = default; Avatar& Avatar::operator=(Avatar&&) = default; QImage Avatar::get(Connection* connection, int dimension, get_callback_t callback) const { return d->get(connection, { dimension, dimension }, move(callback)); } QImage Avatar::get(Connection* connection, int width, int height, get_callback_t callback) const { return d->get(connection, { width, height }, move(callback)); } bool Avatar::upload(Connection* connection, const QString& fileName, upload_callback_t callback) const { if (isJobRunning(d->_uploadRequest)) return false; return d->upload(connection->uploadFile(fileName), move(callback)); } bool Avatar::upload(Connection* connection, QIODevice* source, upload_callback_t callback) const { if (isJobRunning(d->_uploadRequest) || !source->isReadable()) return false; return d->upload(connection->uploadContent(source), move(callback)); } QString Avatar::mediaId() const { return d->_url.authority() + d->_url.path(); } QImage Avatar::Private::get(Connection* connection, QSize size, get_callback_t callback) const { if (!callback) { qCCritical(MAIN) << "Null callbacks are not allowed in Avatar::get"; Q_ASSERT(false); } if (_imageSource == Unknown && _originalImage.load(localFile())) { _imageSource = Cache; _requestedSize = _originalImage.size(); } // Alternating between longer-width and longer-height requests is a sure way // to trick the below code into constantly getting another image from // the server because the existing one is alleged unsatisfactory. // Client authors can only blame themselves if they do so. if (((_imageSource == Unknown && !_thumbnailRequest) || size.width() > _requestedSize.width() || size.height() > _requestedSize.height()) && checkUrl(_url)) { qCDebug(MAIN) << "Getting avatar from" << _url.toString(); _requestedSize = size; if (isJobRunning(_thumbnailRequest)) _thumbnailRequest->abandon(); if (callback) callbacks.emplace_back(move(callback)); _thumbnailRequest = connection->getThumbnail(_url, size); QObject::connect(_thumbnailRequest, &MediaThumbnailJob::success, _thumbnailRequest, [this] { _imageSource = Network; _originalImage = _thumbnailRequest->scaledThumbnail( _requestedSize); _originalImage.save(localFile()); _scaledImages.clear(); for (const auto& n : callbacks) n(); callbacks.clear(); }); } for (const auto& p : _scaledImages) if (p.first == size) return p.second; auto result = _originalImage.isNull() ? QImage() : _originalImage.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); _scaledImages.emplace_back(size, result); return result; } bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t callback) { _uploadRequest = job; if (!isJobRunning(_uploadRequest)) return false; _uploadRequest->connect(_uploadRequest, &BaseJob::success, _uploadRequest, [job, callback] { callback(job->contentUri()); }); return true; } bool Avatar::Private::checkUrl(const QUrl& url) const { if (_imageSource == Banned || url.isEmpty()) return false; // FIXME: Make "mxc" a library-wide constant and maybe even make // the URL checker a Connection(?) method. if (!url.isValid() || url.scheme() != "mxc" || url.path().count('/') != 1) { qCWarning(MAIN) << "Avatar URL is invalid or not mxc-based:" << url.toDisplayString(); _imageSource = Banned; } return _imageSource != Banned; } QString Avatar::Private::localFile() const { static const auto cachePath = cacheLocation(QStringLiteral("avatars")); return cachePath % _url.authority() % '_' % _url.fileName() % ".png"; } QUrl Avatar::url() const { return d->_url; } bool Avatar::updateUrl(const QUrl& newUrl) { if (newUrl == d->_url) return false; d->_url = newUrl; d->_imageSource = Private::Unknown; if (isJobRunning(d->_thumbnailRequest)) d->_thumbnailRequest->abandon(); return true; } spectral/include/libQuotient/lib/qt_connection_util.h0000644000175000000620000001572513566674122023137 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2019 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "util.h" #include namespace Quotient { namespace _impl { template using decorated_slot_tt = std::function; template inline QMetaObject::Connection connectDecorated(SenderT* sender, SignalT signal, ContextT* context, decorated_slot_tt decoratedSlot, Qt::ConnectionType connType) { // See https://bugreports.qt.io/browse/QTBUG-60339 #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) auto pc = std::make_shared(); #else auto pc = std::make_unique(); #endif auto& c = *pc; // Resolve a reference before pc is moved to lambda // Perfect forwarding doesn't work through signal-slot connections - // arguments are always copied (at best - COWed) to the context of // the slot. Therefore the slot decorator receives const ArgTs&... // rather than ArgTs&&... // TODO: std::bind_front() instead of lambda. c = QObject::connect(sender, signal, context, [pc = std::move(pc), decoratedSlot = std::move(decoratedSlot)](const ArgTs&... args) { Q_ASSERT(*pc); // If it's been triggered, it should exist decoratedSlot(*pc, args...); }, connType); return c; } template inline QMetaObject::Connection connectUntil(SenderT* sender, SignalT signal, ContextT* context, std::function functor, Qt::ConnectionType connType) { return connectDecorated(sender, signal, context, decorated_slot_tt( [functor = std::move(functor)](QMetaObject::Connection& c, const ArgTs&... args) { if (functor(args...)) QObject::disconnect(c); }), connType); } template inline QMetaObject::Connection connectSingleShot(SenderT* sender, SignalT signal, ContextT* context, std::function slot, Qt::ConnectionType connType) { return connectDecorated(sender, signal, context, decorated_slot_tt( [slot = std::move(slot)](QMetaObject::Connection& c, const ArgTs&... args) { QObject::disconnect(c); slot(args...); }), connType); } } // namespace _impl /// Create a connection that self-disconnects when its "slot" returns true /*! A slot accepted by connectUntil() is different from classic Qt slots * in that its return value must be bool, not void. The slot's return value * controls whether the connection should be kept; if the slot returns false, * the connection remains; upon returning true, the slot is disconnected from * the signal. Because of a different slot signature connectUntil() doesn't * accept member functions as QObject::connect or Quotient::connectSingleShot * do; you should pass a lambda or a pre-bound member function to it. */ template inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context, const FunctorT& slot, Qt::ConnectionType connType = Qt::AutoConnection) { return _impl::connectUntil(sender, signal, context, wrap_in_function(slot), connType); } /// Create a connection that self-disconnects after triggering on the signal template inline auto connectSingleShot(SenderT* sender, SignalT signal, ContextT* context, const FunctorT& slot, Qt::ConnectionType connType = Qt::AutoConnection) { return _impl::connectSingleShot( sender, signal, context, wrap_in_function(slot), connType); } // Specialisation for usual Qt slots passed as pointers-to-members. template inline auto connectSingleShot(SenderT* sender, SignalT signal, ReceiverT* receiver, void (SlotObjectT::*slot)(ArgTs...), Qt::ConnectionType connType = Qt::AutoConnection) { // TODO: when switching to C++20, use std::bind_front() instead return _impl::connectSingleShot(sender, signal, receiver, wrap_in_function( [receiver, slot](const ArgTs&... args) { (receiver->*slot)(args...); }), connType); } /// A guard pointer that disconnects an interested object upon destruction /*! It's almost QPointer<> except that you have to initialise it with one * more additional parameter - a pointer to a QObject that will be * disconnected from signals of the underlying pointer upon the guard's * destruction. Note that destructing the guide doesn't destruct either QObject. */ template class ConnectionsGuard : public QPointer { public: ConnectionsGuard(T* publisher, QObject* subscriber) : QPointer(publisher), subscriber(subscriber) {} ~ConnectionsGuard() { if (*this) (*this)->disconnect(subscriber); } ConnectionsGuard(ConnectionsGuard&&) = default; ConnectionsGuard& operator=(ConnectionsGuard&&) = default; Q_DISABLE_COPY(ConnectionsGuard) using QPointer::operator=; private: QObject* subscriber; }; } // namespace Quotient spectral/include/libQuotient/lib/user.cpp0000644000175000000620000003351113566674122020541 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "user.h" #include "avatar.h" #include "connection.h" #include "room.h" #include "csapi/content-repo.h" #include "csapi/profile.h" #include "csapi/room_state.h" #include "events/event.h" #include "events/roommemberevent.h" #include #include #include #include #include #include using namespace Quotient; using namespace std::placeholders; using std::move; class User::Private { public: static Avatar makeAvatar(QUrl url) { return Avatar(move(url)); } Private(QString userId, Connection* connection) : userId(move(userId)) , connection(connection) , hueF(stringToHueF(this->userId)) {} QString userId; Connection* connection; QString bridged; QString mostUsedName; QMultiHash otherNames; qreal hueF; Avatar mostUsedAvatar { makeAvatar({}) }; std::vector otherAvatars; auto otherAvatar(const QUrl& url) { return std::find_if(otherAvatars.begin(), otherAvatars.end(), [&url](const auto& av) { return av.url() == url; }); } QMultiHash avatarsToRooms; mutable int totalRooms = 0; QString nameForRoom(const Room* r, const QString& hint = {}) const; void setNameForRoom(const Room* r, QString newName, const QString& oldName); QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const; void setAvatarForRoom(const Room* r, const QUrl& newUrl, const QUrl& oldUrl); void setAvatarOnServer(QString contentUri, User* q); }; QString User::Private::nameForRoom(const Room* r, const QString& hint) const { // If the hint is accurate, this function is O(1) instead of O(n) if (!hint.isNull() && (hint == mostUsedName || otherNames.contains(hint, r))) return hint; return otherNames.key(r, mostUsedName); } static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20; void User::Private::setNameForRoom(const Room* r, QString newName, const QString& oldName) { Q_ASSERT(oldName != newName); Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r)); if (totalRooms < 2) { Q_ASSERT_X(totalRooms > 0 && otherNames.empty(), __FUNCTION__, "Internal structures inconsistency"); mostUsedName = move(newName); return; } otherNames.remove(oldName, r); if (newName != mostUsedName) { // Check if the newName is about to become most used. if (otherNames.count(newName) >= totalRooms - otherNames.size()) { Q_ASSERT(totalRooms > 1); QElapsedTimer et; if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) { qCDebug(MAIN) << "Switching the most used name of user" << userId << "from" << mostUsedName << "to" << newName; qCDebug(MAIN) << "The user is in" << totalRooms << "rooms"; et.start(); } for (auto* r1: connection->allRooms()) if (nameForRoom(r1) == mostUsedName) otherNames.insert(mostUsedName, r1); mostUsedName = newName; otherNames.remove(newName); if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) qCDebug(PROFILER) << et << "to switch the most used name"; } else otherNames.insert(newName, r); } } QUrl User::Private::avatarUrlForRoom(const Room* r, const QUrl& hint) const { // If the hint is accurate, this function is O(1) instead of O(n) if (hint == mostUsedAvatar.url() || avatarsToRooms.contains(hint, r)) return hint; auto it = std::find(avatarsToRooms.begin(), avatarsToRooms.end(), r); return it == avatarsToRooms.end() ? mostUsedAvatar.url() : it.key(); } void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl, const QUrl& oldUrl) { Q_ASSERT(oldUrl != newUrl); Q_ASSERT(oldUrl == mostUsedAvatar.url() || avatarsToRooms.contains(oldUrl, r)); if (totalRooms < 2) { Q_ASSERT_X(totalRooms > 0 && otherAvatars.empty(), __FUNCTION__, "Internal structures inconsistency"); mostUsedAvatar.updateUrl(newUrl); return; } avatarsToRooms.remove(oldUrl, r); if (!avatarsToRooms.contains(oldUrl)) { auto it = otherAvatar(oldUrl); if (it != otherAvatars.end()) otherAvatars.erase(it); } if (newUrl != mostUsedAvatar.url()) { // Check if the new avatar is about to become most used. const auto newUrlUsage = avatarsToRooms.count(newUrl); if (newUrlUsage >= totalRooms - avatarsToRooms.size()) { QElapsedTimer et; if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) { qCInfo(MAIN) << "Switching the most used avatar of user" << userId << "from" << mostUsedAvatar.url().toDisplayString() << "to" << newUrl.toDisplayString(); et.start(); } avatarsToRooms.remove(newUrl); auto nextMostUsedIt = otherAvatar(newUrl); if (nextMostUsedIt == otherAvatars.end()) { qCCritical(MAIN) << userId << "doesn't have" << newUrl.toDisplayString() << "in otherAvatars though it seems to be used in" << newUrlUsage << "rooms"; Q_ASSERT(false); otherAvatars.emplace_back(makeAvatar(newUrl)); nextMostUsedIt = otherAvatars.end() - 1; } std::swap(mostUsedAvatar, *nextMostUsedIt); for (const auto* r1: connection->allRooms()) if (avatarUrlForRoom(r1) == nextMostUsedIt->url()) avatarsToRooms.insert(nextMostUsedIt->url(), r1); if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) qCDebug(PROFILER) << et << "to switch the most used avatar"; } else { if (otherAvatar(newUrl) == otherAvatars.end()) otherAvatars.emplace_back(makeAvatar(newUrl)); avatarsToRooms.insert(newUrl, r); } } } User::User(QString userId, Connection* connection) : QObject(connection), d(new Private(move(userId), connection)) { setObjectName(userId); } Connection* User::connection() const { Q_ASSERT(d->connection); return d->connection; } User::~User() = default; QString User::id() const { return d->userId; } bool User::isGuest() const { Q_ASSERT(!d->userId.isEmpty() && d->userId.startsWith('@')); auto it = std::find_if_not(d->userId.begin() + 1, d->userId.end(), [](QChar c) { return c.isDigit(); }); Q_ASSERT(it != d->userId.end()); return *it == ':'; } int User::hue() const { return int(hueF() * 359); } QString User::name(const Room* room) const { return d->nameForRoom(room); } QString User::rawName(const Room* room) const { return d->bridged.isEmpty() ? name(room) : name(room) % " (" % d->bridged % ')'; } void User::updateName(const QString& newName, const Room* room) { updateName(newName, d->nameForRoom(room), room); } void User::updateName(const QString& newName, const QString& oldName, const Room* room) { Q_ASSERT(oldName == d->mostUsedName || d->otherNames.contains(oldName, room)); if (newName != oldName) { emit nameAboutToChange(newName, oldName, room); d->setNameForRoom(room, newName, oldName); setObjectName(displayname()); emit nameChanged(newName, oldName, room); } } void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, const Room* room) { Q_ASSERT(oldUrl == d->mostUsedAvatar.url() || d->avatarsToRooms.contains(oldUrl, room)); if (newUrl != oldUrl) { d->setAvatarForRoom(room, newUrl, oldUrl); setObjectName(displayname()); emit avatarChanged(this, room); } } void User::rename(const QString& newName) { const auto actualNewName = sanitized(newName); connect(connection()->callApi(id(), actualNewName), &BaseJob::success, this, [=] { updateName(actualNewName); }); } void User::rename(const QString& newName, const Room* r) { if (!r) { qCWarning(MAIN) << "Passing a null room to two-argument User::rename()" "is incorrect; client developer, please fix it"; rename(newName); return; } Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__, "Attempt to rename a user that's not a room member"); const auto actualNewName = sanitized(newName); MemberEventContent evtC; evtC.displayName = actualNewName; connect(r->setState(id(), move(evtC)), &BaseJob::success, this, [=] { updateName(actualNewName, r); }); } bool User::setAvatar(const QString& fileName) { return avatarObject().upload(connection(), fileName, std::bind(&Private::setAvatarOnServer, d.data(), _1, this)); } bool User::setAvatar(QIODevice* source) { return avatarObject().upload(connection(), source, std::bind(&Private::setAvatarOnServer, d.data(), _1, this)); } void User::requestDirectChat() { connection()->requestDirectChat(this); } void User::ignore() { connection()->addToIgnoredUsers(this); } void User::unmarkIgnore() { connection()->removeFromIgnoredUsers(this); } bool User::isIgnored() const { return connection()->isIgnored(this); } void User::Private::setAvatarOnServer(QString contentUri, User* q) { auto* j = connection->callApi(userId, contentUri); connect(j, &BaseJob::success, q, [=] { q->updateAvatarUrl(contentUri, avatarUrlForRoom(nullptr)); }); } QString User::displayname(const Room* room) const { if (room) return room->roomMembername(this); const auto name = d->nameForRoom(nullptr); return name.isEmpty() ? d->userId : name; } QString User::fullName(const Room* room) const { const auto name = d->nameForRoom(room); return name.isEmpty() ? d->userId : name % " (" % d->userId % ')'; } QString User::bridged() const { return d->bridged; } const Avatar& User::avatarObject(const Room* room) const { auto it = d->otherAvatar(d->avatarUrlForRoom(room)); return it != d->otherAvatars.end() ? *it : d->mostUsedAvatar; } QImage User::avatar(int dimension, const Room* room) { return avatar(dimension, dimension, room); } QImage User::avatar(int width, int height, const Room* room) { return avatar(width, height, room, [] {}); } QImage User::avatar(int width, int height, const Room* room, const Avatar::get_callback_t& callback) { return avatarObject(room).get(d->connection, width, height, [=] { emit avatarChanged(this, room); callback(); }); } QString User::avatarMediaId(const Room* room) const { return avatarObject(room).mediaId(); } QUrl User::avatarUrl(const Room* room) const { return avatarObject(room).url(); } void User::processEvent(const RoomMemberEvent& event, const Room* room, bool firstMention) { Q_ASSERT(room); if (firstMention) ++d->totalRooms; if (event.membership() != MembershipType::Invite && event.membership() != MembershipType::Join) return; auto newName = event.displayName(); // `bridged` value uses the same notification signal as the name; // it is assumed that first setting of the bridge occurs together with // the first setting of the name, and further bridge updates are // exceptionally rare (the only reasonable case being that the bridge // changes the naming convention). For the same reason room-specific // bridge tags are not supported at all. QRegularExpression reSuffix( QStringLiteral(" \\((IRC|Gitter|Telegram)\\)$")); auto match = reSuffix.match(newName); if (match.hasMatch()) { if (d->bridged != match.captured(1)) { if (!d->bridged.isEmpty()) qCWarning(MAIN) << "Bridge for user" << id() << "changed:" << d->bridged << "->" << match.captured(1); d->bridged = match.captured(1); } newName.truncate(match.capturedStart(0)); } if (event.prevContent()) { // FIXME: the hint doesn't work for bridged users auto oldNameHint = d->nameForRoom(room, event.prevContent()->displayName); updateName(newName, oldNameHint, room); updateAvatarUrl(event.avatarUrl(), d->avatarUrlForRoom(room, event.prevContent()->avatarUrl), room); } else { updateName(newName, room); updateAvatarUrl(event.avatarUrl(), d->avatarUrlForRoom(room), room); } } qreal User::hueF() const { return d->hueF; } spectral/include/libQuotient/lib/connection.h0000644000175000000620000007610713566674122021377 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "joinstate.h" #include "qt_connection_util.h" #include "csapi/create_room.h" #include "events/accountdataevents.h" #include #include #include #include #include namespace QtOlm { class Account; } namespace Quotient { Q_NAMESPACE class Room; class User; class ConnectionData; class RoomEvent; class SyncJob; class SyncData; class RoomMessagesJob; class PostReceiptJob; class ForgetRoomJob; class MediaThumbnailJob; class JoinRoomJob; class UploadContentJob; class GetContentJob; class DownloadFileJob; class SendToDeviceJob; class SendMessageJob; class LeaveRoomJob; class Connection; using room_factory_t = std::function; using user_factory_t = std::function; /** The default factory to create room objects * * Just a wrapper around operator new. * \sa Connection::setRoomFactory, Connection::setRoomType */ template static inline room_factory_t defaultRoomFactory() { return [](Connection* c, const QString& id, JoinState js) { return new T(c, id, js); }; } /** The default factory to create user objects * * Just a wrapper around operator new. * \sa Connection::setUserFactory, Connection::setUserType */ template static inline user_factory_t defaultUserFactory() { return [](Connection* c, const QString& id) { return new T(id, c); }; } /** Enumeration with flags defining the network job running policy * So far only background/foreground flags are available. * * \sa Connection::callApi, Connection::run */ enum RunningPolicy { ForegroundRequest = 0x0, BackgroundRequest = 0x1 }; Q_ENUM_NS(RunningPolicy) class Connection : public QObject { Q_OBJECT Q_PROPERTY(User* localUser READ user NOTIFY stateChanged) Q_PROPERTY(QString localUserId READ userId NOTIFY stateChanged) Q_PROPERTY(QString deviceId READ deviceId NOTIFY stateChanged) Q_PROPERTY(QByteArray accessToken READ accessToken NOTIFY stateChanged) Q_PROPERTY(QString defaultRoomVersion READ defaultRoomVersion NOTIFY capabilitiesLoaded) Q_PROPERTY(QUrl homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) Q_PROPERTY(QString domain READ domain NOTIFY homeserverChanged) Q_PROPERTY(bool cacheState READ cacheState WRITE setCacheState NOTIFY cacheStateChanged) Q_PROPERTY(bool lazyLoading READ lazyLoading WRITE setLazyLoading NOTIFY lazyLoadingChanged) public: // Room ids, rather than room pointers, are used in the direct chat // map types because the library keeps Invite rooms separate from // rooms in Join and Leave state; and direct chats in account data // are stored with no regard to their state. using DirectChatsMap = QMultiHash; using DirectChatUsersMap = QMultiHash; using IgnoredUsersList = IgnoredUsersEvent::content_type; using UsersToDevicesToEvents = UnorderedMap>; enum RoomVisibility { PublishRoom, UnpublishRoom }; // FIXME: Should go inside CreateRoomJob explicit Connection(QObject* parent = nullptr); explicit Connection(const QUrl& server, QObject* parent = nullptr); ~Connection() override; /// Get all Invited and Joined rooms /*! * \return a hashmap from a composite key - room name and whether * it's an Invite rather than Join - to room pointers * \sa allRooms, rooms, roomsWithTag */ [[deprecated("Use allRooms(), roomsWithTag() or rooms(joinStates) instead")]] QHash, Room*> roomMap() const; /// Get all rooms known within this Connection /*! * This includes Invite, Join and Leave rooms, in no particular order. * \note Leave rooms will only show up in the list if they have been left * in the same running session. The library doesn't cache left rooms * between runs and it doesn't retrieve the full list of left rooms * from the server. * \sa rooms, room, roomsWithTag */ Q_INVOKABLE QVector allRooms() const; /// Get rooms that have either of the given join state(s) /*! * This method returns, in no particular order, rooms which join state * matches the mask passed in \p joinStates. * \note Similar to allRooms(), this won't retrieve the full list of * Leave rooms from the server. * \sa allRooms, room, roomsWithTag */ Q_INVOKABLE QVector rooms(JoinStates joinStates) const; /// Get the total number of rooms in the given join state(s) Q_INVOKABLE int roomsCount(JoinStates joinStates) const; /** Check whether the account has data of the given type * Direct chats map is not supported by this method _yet_. */ bool hasAccountData(const QString& type) const; /** Get a generic account data event of the given type * This returns an account data event of the given type * stored on the server. Direct chats map cannot be retrieved * using this method _yet_; use directChats() instead. */ const EventPtr& accountData(const QString& type) const; /** Get a generic account data event of the given type * This returns an account data event of the given type * stored on the server. Direct chats map cannot be retrieved * using this method _yet_; use directChats() instead. */ template const typename EventT::content_type accountData() const { if (const auto& eventPtr = accountData(EventT::matrixTypeId())) return eventPtr->content(); return {}; } /** Get account data as a JSON object * This returns the content part of the account data event * of the given type. Direct chats map cannot be retrieved using * this method _yet_; use directChats() instead. */ Q_INVOKABLE QJsonObject accountDataJson(const QString& type) const; /** Set a generic account data event of the given type */ void setAccountData(EventPtr&& event); Q_INVOKABLE void setAccountData(const QString& type, const QJsonObject& content); /** Get all Invited and Joined rooms grouped by tag * \return a hashmap from tag name to a vector of room pointers, * sorted by their order in the tag - details are at * https://matrix.org/speculator/spec/drafts%2Fe2e/client_server/unstable.html#id95 */ QHash> tagsToRooms() const; /** Get all room tags known on this connection */ QStringList tagNames() const; /** Get the list of rooms with the specified tag */ QVector roomsWithTag(const QString& tagName) const; /** Mark the room as a direct chat with the user * This function marks \p room as a direct chat with \p user. * Emits the signal synchronously, without waiting to complete * synchronisation with the server. * * \sa directChatsListChanged */ void addToDirectChats(const Room* room, User* user); /** Unmark the room from direct chats * This function removes the room id from direct chats either for * a specific \p user or for all users if \p user in nullptr. * The room id is used to allow removal of, e.g., ids of forgotten * rooms; a Room object need not exist. Emits the signal * immediately, without waiting to complete synchronisation with * the server. * * \sa directChatsListChanged */ void removeFromDirectChats(const QString& roomId, User* user = nullptr); /** Check whether the room id corresponds to a direct chat */ bool isDirectChat(const QString& roomId) const; /** Get the whole map from users to direct chat rooms */ DirectChatsMap directChats() const; /** Retrieve the list of users the room is a direct chat with * @return The list of users for which this room is marked as * a direct chat; an empty list if the room is not a direct chat */ QList directChatUsers(const Room* room) const; /** Check whether a particular user is in the ignore list */ Q_INVOKABLE bool isIgnored(const User* user) const; /** Get the whole list of ignored users */ Q_INVOKABLE IgnoredUsersList ignoredUsers() const; /** Add the user to the ignore list * The change signal is emitted synchronously, without waiting * to complete synchronisation with the server. * * \sa ignoredUsersListChanged */ Q_INVOKABLE void addToIgnoredUsers(const User* user); /** Remove the user from the ignore list */ /** Similar to adding, the change signal is emitted synchronously. * * \sa ignoredUsersListChanged */ Q_INVOKABLE void removeFromIgnoredUsers(const User* user); /** Get the full list of users known to this account */ QMap users() const; /** Get the base URL of the homeserver to connect to */ QUrl homeserver() const; /** Get the domain name used for ids/aliases on the server */ QString domain() const; /** Find a room by its id and a mask of applicable states */ Q_INVOKABLE Room* room(const QString& roomId, JoinStates states = JoinState::Invite | JoinState::Join) const; /** Find a room by its alias and a mask of applicable states */ Q_INVOKABLE Room* roomByAlias(const QString& roomAlias, JoinStates states = JoinState::Invite | JoinState::Join) const; /** Update the internal map of room aliases to IDs */ /// This is used to maintain the internal index of room aliases. /// It does NOT change aliases on the server, /// \sa Room::setLocalAliases void updateRoomAliases(const QString& roomId, const QString& aliasServer, const QStringList& previousRoomAliases, const QStringList& roomAliases); Q_INVOKABLE Room* invitation(const QString& roomId) const; Q_INVOKABLE User* user(const QString& userId); const User* user() const; User* user(); QString userId() const; QString deviceId() const; QByteArray accessToken() const; QtOlm::Account* olmAccount() const; Q_INVOKABLE SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; Q_INVOKABLE void getTurnServers(); struct SupportedRoomVersion { QString id; QString status; static const QString StableTag; // "stable", as of CS API 0.5 bool isStable() const { return status == StableTag; } friend QDebug operator<<(QDebug dbg, const SupportedRoomVersion& v) { QDebugStateSaver _(dbg); return dbg.nospace() << v.id << '/' << v.status; } }; /// Get the room version recommended by the server /** Only works after server capabilities have been loaded. * \sa loadingCapabilities */ QString defaultRoomVersion() const; /// Get the room version considered stable by the server /** Only works after server capabilities have been loaded. * \sa loadingCapabilities */ QStringList stableRoomVersions() const; /// Get all room versions supported by the server /** Only works after server capabilities have been loaded. * \sa loadingCapabilities */ QVector availableRoomVersions() const; /** * Call this before first sync to load from previously saved file. * * \param fromFile A local path to read the state from. Uses QUrl * to be QML-friendly. Empty parameter means saving to the directory * defined by stateCachePath() / stateCacheDir(). */ Q_INVOKABLE void loadState(); /** * This method saves the current state of rooms (but not messages * in them) to a local cache file, so that it could be loaded by * loadState() on a next run of the client. * * \param toFile A local path to save the state to. Uses QUrl to be * QML-friendly. Empty parameter means saving to the directory * defined by stateCachePath() / stateCacheDir(). */ Q_INVOKABLE void saveState() const; /// This method saves the current state of a single room. void saveRoomState(Room* r) const; /// Get the default directory path to save the room state to /** \sa stateCacheDir */ Q_INVOKABLE QString stateCachePath() const; /// Get the default directory to save the room state to /** * This function returns the default directory to store the cached * room state, defined as follows: * \code * QStandardPaths::writeableLocation(QStandardPaths::CacheLocation) + * _safeUserId + "_state.json" \endcode where `_safeUserId` is userId() with * `:` (colon) replaced by * `_` (underscore), as colons are reserved characters on Windows. * \sa loadState, saveState, stateCachePath */ QDir stateCacheDir() const; /** Whether or not the rooms state should be cached locally * \sa loadState(), saveState() */ bool cacheState() const; void setCacheState(bool newValue); bool lazyLoading() const; void setLazyLoading(bool newValue); /*! Start a pre-created job object on this connection */ void run(BaseJob* job, RunningPolicy runningPolicy = ForegroundRequest) const; /*! Start a job of a specified type with specified arguments and policy * * This is a universal method to create and start a job of a type passed * as a template parameter. The policy allows to fine-tune the way * the job is executed - as of this writing it means a choice * between "foreground" and "background". * * \param runningPolicy controls how the job is executed * \param jobArgs arguments to the job constructor * * \sa BaseJob::isBackground. QNetworkRequest::BackgroundRequestAttribute */ template JobT* callApi(RunningPolicy runningPolicy, JobArgTs&&... jobArgs) const { auto job = new JobT(std::forward(jobArgs)...); run(job, runningPolicy); return job; } /*! Start a job of a specified type with specified arguments * * This is an overload that runs the job with "foreground" policy. */ template JobT* callApi(JobArgTs&&... jobArgs) const { return callApi(ForegroundRequest, std::forward(jobArgs)...); } /** Generate a new transaction id. Transaction id's are unique within * a single Connection object */ Q_INVOKABLE QByteArray generateTxnId() const; /// Set a room factory function static void setRoomFactory(room_factory_t f); /// Set a user factory function static void setUserFactory(user_factory_t f); /// Get a room factory function static room_factory_t roomFactory(); /// Get a user factory function static user_factory_t userFactory(); /// Set the room factory to default with the overriden room type template static void setRoomType() { setRoomFactory(defaultRoomFactory()); } /// Set the user factory to default with the overriden user type template static void setUserType() { setUserFactory(defaultUserFactory()); } public slots: /** Set the homeserver base URL */ void setHomeserver(const QUrl& baseUrl); /** Determine and set the homeserver from MXID */ void resolveServer(const QString& mxid); void connectToServer(const QString& user, const QString& password, const QString& initialDeviceName, const QString& deviceId = {}); void connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId); /// Explicitly request capabilities from the server void reloadCapabilities(); /// Find out if capabilites are still loading from the server bool loadingCapabilities() const; /** @deprecated Use stopSync() instead */ void disconnectFromServer() { stopSync(); } void logout(); void sync(int timeout = -1); void syncLoop(int timeout = -1); void stopSync(); QString nextBatchToken() const; virtual MediaThumbnailJob* getThumbnail(const QString& mediaId, QSize requestedSize, RunningPolicy policy = BackgroundRequest) const; MediaThumbnailJob* getThumbnail(const QUrl& url, QSize requestedSize, RunningPolicy policy = BackgroundRequest) const; MediaThumbnailJob* getThumbnail(const QUrl& url, int requestedWidth, int requestedHeight, RunningPolicy policy = BackgroundRequest) const; // QIODevice* should already be open UploadContentJob* uploadContent(QIODevice* contentSource, const QString& filename = {}, const QString& overrideContentType = {}) const; UploadContentJob* uploadFile(const QString& fileName, const QString& overrideContentType = {}); GetContentJob* getContent(const QString& mediaId) const; GetContentJob* getContent(const QUrl& url) const; // If localFilename is empty, a temporary file will be created DownloadFileJob* downloadFile(const QUrl& url, const QString& localFilename = {}) const; /** * \brief Create a room (generic method) * This method allows to customize room entirely to your liking, * providing all the attributes the original CS API provides. */ CreateRoomJob* createRoom(RoomVisibility visibility, const QString& alias, const QString& name, const QString& topic, QStringList invites, const QString& presetName = {}, const QString& roomVersion = {}, bool isDirect = false, const QVector& initialState = {}, const QVector& invite3pids = {}, const QJsonObject& creationContent = {}); /** Get a direct chat with a single user * This method may return synchronously or asynchoronously depending * on whether a direct chat room with the respective person exists * already. * * \sa directChatAvailable */ void requestDirectChat(const QString& userId); /** Get a direct chat with a single user * This method may return synchronously or asynchoronously depending * on whether a direct chat room with the respective person exists * already. * * \sa directChatAvailable */ void requestDirectChat(User* u); /** Run an operation in a direct chat with the user * This method may return synchronously or asynchoronously depending * on whether a direct chat room with the respective person exists * already. Instead of emitting a signal it executes the passed * function object with the direct chat room as its parameter. */ void doInDirectChat(const QString& userId, const std::function& operation); /** Run an operation in a direct chat with the user * This method may return synchronously or asynchoronously depending * on whether a direct chat room with the respective person exists * already. Instead of emitting a signal it executes the passed * function object with the direct chat room as its parameter. */ void doInDirectChat(User* u, const std::function& operation); /** Create a direct chat with a single user, optional name and topic * A room will always be created, unlike in requestDirectChat. * It is advised to use requestDirectChat as a default way of getting * one-on-one with a person, and only use createDirectChat when * a new creation is explicitly desired. */ CreateRoomJob* createDirectChat(const QString& userId, const QString& topic = {}, const QString& name = {}); virtual JoinRoomJob* joinRoom(const QString& roomAlias, const QStringList& serverNames = {}); /** Sends /forget to the server and also deletes room locally. * This method is in Connection, not in Room, since it's a * room lifecycle operation, and Connection is an acting room manager. * It ensures that the local user is not a member of a room (running /leave, * if necessary) then issues a /forget request and if that one doesn't fail * deletion of the local Room object is ensured. * \param id - the room id to forget * \return - the ongoing /forget request to the server; note that the * success() signal of this request is connected to deleteLater() * of a respective room so by the moment this finishes, there might be no * Room object anymore. */ ForgetRoomJob* forgetRoom(const QString& id); SendToDeviceJob* sendToDevices(const QString& eventType, const UsersToDevicesToEvents& eventsMap) const; /** \deprecated This method is experimental and may be removed any time */ SendMessageJob* sendMessage(const QString& roomId, const RoomEvent& event) const; /** \deprecated Do not use this directly, use Room::leaveRoom() instead */ virtual LeaveRoomJob* leaveRoom(Room* room); // Old API that will be abolished any time soon. DO NOT USE. /** @deprecated Use callApi() or Room::postReceipt() instead */ virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event) const; signals: /** * @deprecated * This was a signal resulting from a successful resolveServer(). * Since Connection now provides setHomeserver(), the HS URL * may change even without resolveServer() invocation. Use * homeserverChanged() instead of resolved(). You can also use * connectToServer and connectWithToken without the HS URL set in * advance (i.e. without calling resolveServer), as they now trigger * server name resolution from MXID if the server URL is not valid. */ void resolved(); void resolveError(QString error); void homeserverChanged(QUrl baseUrl); void capabilitiesLoaded(); void connected(); void reconnected(); //< \deprecated Use connected() instead void loggedOut(); /** Login data or state have changed * * This is a common change signal for userId, deviceId and * accessToken - these properties normally only change at * a successful login and logout and are constant at other times. */ void stateChanged(); void loginError(QString message, QString details); /** A network request (job) failed * * @param request - the pointer to the failed job */ void requestFailed(BaseJob* request); /** A network request (job) failed due to network problems * * This is _only_ emitted when the job will retry on its own; * once it gives up, requestFailed() will be emitted. * * @param message - message about the network problem * @param details - raw error details, if any available * @param retriesTaken - how many retries have already been taken * @param nextRetryInMilliseconds - when the job will retry again */ void networkError(QString message, QString details, int retriesTaken, int nextRetryInMilliseconds); void syncDone(); void syncError(QString message, QString details); void newUser(User* user); /** * \group Signals emitted on room transitions * * Note: Rooms in Invite state are always stored separately from * rooms in Join/Leave state, because of special treatment of * invite_state in Matrix CS API (see The Spec on /sync for details). * Therefore, objects below are: r - room in Join/Leave state; * i - room in Invite state * * 1. none -> Invite: newRoom(r), invitedRoom(r,nullptr) * 2. none -> Join: newRoom(r), joinedRoom(r,nullptr) * 3. none -> Leave: newRoom(r), leftRoom(r,nullptr) * 4. Invite -> Join: * newRoom(r), joinedRoom(r,i), aboutToDeleteRoom(i) * 4a. Leave and Invite -> Join: * joinedRoom(r,i), aboutToDeleteRoom(i) * 5. Invite -> Leave: * newRoom(r), leftRoom(r,i), aboutToDeleteRoom(i) * 5a. Leave and Invite -> Leave: * leftRoom(r,i), aboutToDeleteRoom(i) * 6. Join -> Leave: leftRoom(r) * 7. Leave -> Invite: newRoom(i), invitedRoom(i,r) * 8. Leave -> Join: joinedRoom(r) * The following transitions are only possible via forgetRoom() * so far; if a room gets forgotten externally, sync won't tell * about it: * 9. any -> none: as any -> Leave, then aboutToDeleteRoom(r) */ /** A new room object has been created */ void newRoom(Room* room); /** A room invitation is seen for the first time * * If the same room is in Left state, it's passed in prev. Beware * that initial sync will trigger this signal for all rooms in * Invite state. */ void invitedRoom(Room* room, Room* prev); /** A joined room is seen for the first time * * It's not the same as receiving a room in "join" section of sync * response (rooms will be there even after joining); it's also * not (exactly) the same as actual joining action of a user (all * rooms coming in initial sync will trigger this signal too). If * this room was in Invite state before, the respective object is * passed in prev (and it will be deleted shortly afterwards). */ void joinedRoom(Room* room, Room* prev); /** A room has just been left * * If this room has been in Invite state (as in case of rejecting * an invitation), the respective object will be passed in prev * (and will be deleted shortly afterwards). Note that, similar * to invitedRoom and joinedRoom, this signal is triggered for all * Left rooms upon initial sync (not only those that were left * right before the sync). */ void leftRoom(Room* room, Room* prev); /** The room object is about to be deleted */ void aboutToDeleteRoom(Room* room); /** The room has just been created by createRoom or requestDirectChat * * This signal is not emitted in usual room state transitions, * only as an outcome of room creation operations invoked by * the client. * \note requestDirectChat doesn't necessarily create a new chat; * use directChatAvailable signal if you just need to obtain * a direct chat room. */ void createdRoom(Room* room); /** The first sync for the room has been completed * * This signal is emitted after the room has been synced the first * time. This is the right signal to connect to if you need to * access the room state (name, aliases, members); state transition * signals (newRoom, joinedRoom etc.) come earlier, when the room * has just been created. */ void loadedRoomState(Room* room); /** Account data (except direct chats) have changed */ void accountDataChanged(QString type); /** The direct chat room is ready for using * This signal is emitted upon any successful outcome from * requestDirectChat. */ void directChatAvailable(Room* directChat); /** The list of direct chats has changed * This signal is emitted every time when the mapping of users * to direct chat rooms is changed (because of either local updates * or a different list arrived from the server). */ void directChatsListChanged(DirectChatsMap additions, DirectChatsMap removals); void ignoredUsersListChanged(IgnoredUsersList additions, IgnoredUsersList removals); void cacheStateChanged(); void lazyLoadingChanged(); void turnServersChanged(const QJsonObject& servers); protected: /** * @brief Access the underlying ConnectionData class */ const ConnectionData* connectionData() const; /** Get a Room object for the given id in the given state * * Use this method when you need a Room object in the local list * of rooms, with the given state. Note that this does not interact * with the server; in particular, does not automatically create * rooms on the server. This call performs necessary join state * transitions; e.g., if it finds a room in Invite but * `joinState == JoinState::Join` then the Invite room object * will be deleted and a new room object with Join state created. * In contrast, switching between Join and Leave happens within * the same object. * \param roomId room id (not alias!) * \param joinState desired (target) join state of the room; if * omitted, any state will be found and return unchanged, or a * new Join room created. * @return a pointer to a Room object with the specified id and the * specified state; nullptr if roomId is empty or if roomFactory() * failed to create a Room object. */ Room* provideRoom(const QString& roomId, Omittable joinState = none); /** * Completes loading sync data. */ void onSyncSuccess(SyncData&& data, bool fromCache = false); protected slots: void syncLoopIteration(); private: class Private; QScopedPointer d; /** * A single entry for functions that need to check whether the * homeserver is valid before running. May either execute connectFn * synchronously or asynchronously (if tryResolve is true and * a DNS lookup is initiated); in case of errors, emits resolveError * if the homeserver URL is not valid and cannot be resolved from * userId. * * @param userId - fully-qualified MXID to resolve HS from * @param connectFn - a function to execute once the HS URL is good */ void checkAndConnect(const QString& userId, std::function connectFn); void doConnectToServer(const QString& user, const QString& password, const QString& initialDeviceName, const QString& deviceId = {}); static room_factory_t _roomFactory; static user_factory_t _userFactory; }; } // namespace Quotient Q_DECLARE_METATYPE(Quotient::Connection*) spectral/include/libQuotient/lib/user.h0000644000175000000620000001443513566674122020212 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "avatar.h" #include #include namespace Quotient { class Connection; class Room; class RoomMemberEvent; class User : public QObject { Q_OBJECT Q_PROPERTY(QString id READ id CONSTANT) Q_PROPERTY(bool isGuest READ isGuest CONSTANT) Q_PROPERTY(int hue READ hue CONSTANT) Q_PROPERTY(qreal hueF READ hueF CONSTANT) Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString displayName READ displayname NOTIFY nameChanged STORED false) Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged STORED false) Q_PROPERTY(QString bridgeName READ bridged NOTIFY nameChanged STORED false) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) public: User(QString userId, Connection* connection); ~User() override; Connection* connection() const; /** Get unique stable user id * User id is generated by the server and is not changed ever. */ QString id() const; /** Get the name chosen by the user * This may be empty if the user didn't choose the name or cleared * it. If the user is bridged, the bridge postfix (such as '(IRC)') * is stripped out. No disambiguation for the room is done. * \sa displayName, rawName */ QString name(const Room* room = nullptr) const; /** Get the user name along with the bridge postfix * This function is similar to name() but appends the bridge postfix * (such as '(IRC)') to the user name. No disambiguation is done. * \sa name, displayName */ QString rawName(const Room* room = nullptr) const; /** Get the displayed user name * When \p room is null, this method returns result of name() if * the name is non-empty; otherwise it returns user id. * When \p room is non-null, this call is equivalent to * Room::roomMembername invocation for the user (i.e. the user's * disambiguated room-specific name is returned). * \sa name, id, fullName, Room::roomMembername */ QString displayname(const Room* room = nullptr) const; /** Get user name and id in one string * The constructed string follows the format 'name (id)' * which the spec recommends for users disambiguation in * a room context and in other places. * \sa displayName, Room::roomMembername */ QString fullName(const Room* room = nullptr) const; /** * Returns the name of bridge the user is connected from or empty. */ QString bridged() const; /** Whether the user is a guest * As of now, the function relies on the convention used in Synapse * that guests and only guests have all-numeric IDs. This may or * may not work with non-Synapse servers. */ bool isGuest() const; /** Hue color component of this user based on id. * The implementation is based on XEP-0392: * https://xmpp.org/extensions/xep-0392.html * Naming and ranges are the same as QColor's hue methods: * https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision */ int hue() const; qreal hueF() const; const Avatar& avatarObject(const Room* room = nullptr) const; Q_INVOKABLE QImage avatar(int dimension, const Room* room = nullptr); Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight, const Room* room = nullptr); QImage avatar(int width, int height, const Room* room, const Avatar::get_callback_t& callback); QString avatarMediaId(const Room* room = nullptr) const; QUrl avatarUrl(const Room* room = nullptr) const; /// This method is for internal use and should not be called /// from client code // FIXME: Move it away to private in lib 0.6 void processEvent(const RoomMemberEvent& event, const Room* r, bool firstMention); public slots: /** Set a new name in the global user profile */ void rename(const QString& newName); /** Set a new name for the user in one room */ void rename(const QString& newName, const Room* r); /** Upload the file and use it as an avatar */ bool setAvatar(const QString& fileName); /** Upload contents of the QIODevice and set that as an avatar */ bool setAvatar(QIODevice* source); /** Create or find a direct chat with this user * The resulting chat is returned asynchronously via * Connection::directChatAvailable() */ void requestDirectChat(); /** Add the user to the ignore list */ void ignore(); /** Remove the user from the ignore list */ void unmarkIgnore(); /** Check whether the user is in ignore list */ bool isIgnored() const; signals: void nameAboutToChange(QString newName, QString oldName, const Room* roomContext); void nameChanged(QString newName, QString oldName, const Room* roomContext); void avatarChanged(User* user, const Room* roomContext); private slots: void updateName(const QString& newName, const Room* room = nullptr); void updateName(const QString& newName, const QString& oldName, const Room* room = nullptr); void updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, const Room* room = nullptr); private: class Private; QScopedPointer d; }; } // namespace Quotient Q_DECLARE_METATYPE(Quotient::User*) spectral/include/libQuotient/lib/room.cpp0000644000175000000620000030742113566674122020543 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "room.h" #include "avatar.h" #include "connection.h" #include "converters.h" #include "e2ee.h" #include "syncdata.h" #include "user.h" #include "csapi/account-data.h" #include "csapi/banning.h" #include "csapi/inviting.h" #include "csapi/kicking.h" #include "csapi/leaving.h" #include "csapi/receipts.h" #include "csapi/redaction.h" #include "csapi/room_send.h" #include "csapi/room_state.h" #include "csapi/room_upgrades.h" #include "csapi/rooms.h" #include "csapi/tags.h" #include "events/callanswerevent.h" #include "events/callcandidatesevent.h" #include "events/callhangupevent.h" #include "events/callinviteevent.h" #include "events/encryptionevent.h" #include "events/reactionevent.h" #include "events/receiptevent.h" #include "events/redactionevent.h" #include "events/roomavatarevent.h" #include "events/roomcreateevent.h" #include "events/roommemberevent.h" #include "events/roomtombstoneevent.h" #include "events/simplestateevents.h" #include "events/typingevent.h" #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/postreadmarkersjob.h" #include #include #include #include #include #include // for efficient string concats (operator%) #include #include #include #include #include // QtOlm #include // QtOlm #include // QtOlm using namespace Quotient; using namespace std::placeholders; using std::move; #if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123) using std::llround; #endif enum EventsPlacement : int { Older = -1, Newer = 1 }; class Room::Private { public: /// Map of user names to users /** User names potentially duplicate, hence QMultiHash. */ using members_map_t = QMultiHash; Private(Connection* c, QString id_, JoinState initialJoinState) : q(nullptr), connection(c), id(move(id_)), joinState(initialJoinState) {} Room* q; Connection* connection; QString id; JoinState joinState; RoomSummary summary = { none, 0, none }; /// The state of the room at timeline position before-0 /// \sa timelineBase UnorderedMap baseState; /// State event stubs - events without content, just type and state key static decltype(baseState) stubbedState; /// The state of the room at timeline position after-maxTimelineIndex() /// \sa Room::syncEdge QHash currentState; /// Servers with aliases for this room except the one of the local user /// \sa Room::remoteAliases QSet aliasServers; Timeline timeline; PendingEvents unsyncedEvents; QHash eventsIndex; // A map from evtId to a map of relation type to a vector of event // pointers. Not using QMultiHash, because we want to quickly return // a number of relations for a given event without enumerating them. QHash, RelatedEvents> relations; QString displayname; Avatar avatar; int highlightCount = 0; int notificationCount = 0; members_map_t membersMap; QList usersTyping; QMultiHash eventIdReadUsers; QList usersInvited; QList membersLeft; int unreadMessages = 0; bool displayed = false; QString firstDisplayedEventId; QString lastDisplayedEventId; QHash lastReadEventIds; QString serverReadMarker; TagsMap tags; UnorderedMap accountData; QString prevBatch; QPointer eventsHistoryJob; QPointer allMembersJob; struct FileTransferPrivateInfo { FileTransferPrivateInfo() = default; FileTransferPrivateInfo(BaseJob* j, const QString& fileName, bool isUploading = false) : status(FileTransferInfo::Started) , job(j) , localFileInfo(fileName) , isUpload(isUploading) {} FileTransferInfo::Status status = FileTransferInfo::None; QPointer job = nullptr; QFileInfo localFileInfo {}; bool isUpload = false; qint64 progress = 0; qint64 total = -1; void update(qint64 p, qint64 t) { if (t == 0) { t = -1; if (p == 0) p = -1; } if (p != -1) qCDebug(PROFILER) << "Transfer progress:" << p << "/" << t << "=" << llround(double(p) / t * 100) << "%"; progress = p; total = t; } }; void failedTransfer(const QString& tid, const QString& errorMessage = {}) { qCWarning(MAIN) << "File transfer failed for id" << tid; if (!errorMessage.isEmpty()) qCWarning(MAIN) << "Message:" << errorMessage; fileTransfers[tid].status = FileTransferInfo::Failed; emit q->fileTransferFailed(tid, errorMessage); } /// A map from event/txn ids to information about the long operation; /// used for both download and upload operations QHash fileTransfers; const RoomMessageEvent* getEventWithFile(const QString& eventId) const; QString fileNameToDownload(const RoomMessageEvent* event) const; Changes setSummary(RoomSummary&& newSummary); // void inviteUser(User* u); // We might get it at some point in time. void insertMemberIntoMap(User* u); void renameMember(User* u, const QString& oldName); void removeMemberFromMap(const QString& username, User* u); // This updates the room displayname field (which is the way a room // should be shown in the room list); called whenever the list of // members, the room name (m.room.name) or canonical alias change. void updateDisplayname(); // This is used by updateDisplayname() but only calculates the new name // without any updates. QString calculateDisplayname() const; /// A point in the timeline corresponding to baseState rev_iter_t timelineBase() const { return q->findInTimeline(-1); } void getPreviousContent(int limit = 10); const StateEventBase* getCurrentState(const StateEventKey& evtKey) const { const auto* evt = currentState.value(evtKey, nullptr); if (!evt) { if (stubbedState.find(evtKey) == stubbedState.end()) { // In the absence of a real event, make a stub as-if an event // with empty content has been received. Event classes should be // prepared for empty/invalid/malicious content anyway. stubbedState.emplace(evtKey, loadStateEvent(evtKey.first, {}, evtKey.second)); qCDebug(STATE) << "A new stub event created for key {" << evtKey.first << evtKey.second << "}"; } evt = stubbedState[evtKey].get(); Q_ASSERT(evt); } Q_ASSERT(evt->matrixType() == evtKey.first && evt->stateKey() == evtKey.second); return evt; } template const EventT* getCurrentState(const QString& stateKey = {}) const { const auto* evt = getCurrentState({ EventT::matrixTypeId(), stateKey }); Q_ASSERT(evt->type() == EventT::typeId() && evt->matrixType() == EventT::matrixTypeId()); return static_cast(evt); } bool isEventNotable(const TimelineItem& ti) const { return !ti->isRedacted() && ti->senderId() != connection->userId() && is(*ti); } template Changes updateStateFrom(EventArrayT&& events) { Changes changes = NoChange; if (!events.empty()) { QElapsedTimer et; et.start(); for (auto&& eptr : events) { const auto& evt = *eptr; Q_ASSERT(evt.isStateEvent()); // Update baseState afterwards to make sure that the old state // is valid and usable inside processStateEvent changes |= q->processStateEvent(evt); baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr); } if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** Room::Private::updateStateFrom():" << events.size() << "event(s)," << et; } return changes; } Changes addNewMessageEvents(RoomEvents&& events); void addHistoricalMessageEvents(RoomEvents&& events); /** Move events into the timeline * * Insert events into the timeline, either new or historical. * Pointers in the original container become empty, the ownership * is passed to the timeline container. * @param events - the range of events to be inserted * @param placement - position and direction of insertion: Older for * historical messages, Newer for new ones */ Timeline::size_type moveEventsToTimeline(RoomEventsRange events, EventsPlacement placement); /** * Remove events from the passed container that are already in the timeline */ void dropDuplicateEvents(RoomEvents& events) const; Changes setLastReadEvent(User* u, QString eventId); void updateUnreadCount(rev_iter_t from, rev_iter_t to); Changes promoteReadMarker(User* u, rev_iter_t newMarker, bool force = false); Changes markMessagesAsRead(rev_iter_t upToMarker); void getAllMembers(); QString sendEvent(RoomEventPtr&& event); template QString sendEvent(ArgTs&&... eventArgs) { return sendEvent(makeEvent(std::forward(eventArgs)...)); } RoomEvent* addAsPending(RoomEventPtr&& event); QString doSendEvent(const RoomEvent* pEvent); void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); SetRoomStateWithKeyJob* requestSetState(const StateEventBase& event) { // if (event.roomId().isEmpty()) // event.setRoomId(id); // if (event.senderId().isEmpty()) // event.setSender(connection->userId()); // TODO: Queue up state events sending (see #133). // TODO: Maybe addAsPending() as well, despite having no txnId return connection->callApi( id, event.matrixType(), event.stateKey(), event.contentJson()); } template auto requestSetState(ArgTs&&... args) { return requestSetState(EvT(std::forward(args)...)); } /*! Apply redaction to the timeline * * Tries to find an event in the timeline and redact it; deletes the * redaction event whether the redacted event was found or not. * \return true if the event has been found and redacted; false otherwise */ bool processRedaction(const RedactionEvent& redaction); /*! Apply a new revision of the event to the timeline * * Tries to find an event in the timeline and replace it with the new * content passed in \p newMessage. * \return true if the event has been found and replaced; false otherwise */ bool processReplacement(const RoomMessageEvent& newEvent); void setTags(TagsMap newTags); QJsonObject toJson() const; private: using users_shortlist_t = std::array; template users_shortlist_t buildShortlist(const ContT& users) const; users_shortlist_t buildShortlist(const QStringList& userIds) const; bool isLocalUser(const User* u) const { return u == q->localUser(); } }; decltype(Room::Private::baseState) Room::Private::stubbedState {}; Room::Room(Connection* connection, QString id, JoinState initialJoinState) : QObject(connection), d(new Private(connection, id, initialJoinState)) { setObjectName(id); // See "Accessing the Public Class" section in // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ d->q = this; d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name connectUntil(connection, &Connection::loadedRoomState, this, [this](Room* r) { if (this == r) emit baseStateLoaded(); return this == r; // loadedRoomState fires only once per room }); qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id; } Room::~Room() { delete d; } const QString& Room::id() const { return d->id; } QString Room::version() const { const auto v = d->getCurrentState()->version(); return v.isEmpty() ? QStringLiteral("1") : v; } bool Room::isUnstable() const { return !connection()->loadingCapabilities() && !connection()->stableRoomVersions().contains(version()); } QString Room::predecessorId() const { return d->getCurrentState()->predecessor().roomId; } QString Room::successorId() const { return d->getCurrentState()->successorRoomId(); } const Room::Timeline& Room::messageEvents() const { return d->timeline; } const Room::PendingEvents& Room::pendingEvents() const { return d->unsyncedEvents; } bool Room::allHistoryLoaded() const { return !d->timeline.empty() && is(*d->timeline.front()); } QString Room::name() const { return d->getCurrentState()->name(); } QStringList Room::localAliases() const { return d ->getCurrentState( connection()->domain()) ->aliases(); } QStringList Room::remoteAliases() const { QStringList result; for (const auto& s : d->aliasServers) result += d->getCurrentState(s)->aliases(); return result; } QString Room::canonicalAlias() const { return d->getCurrentState()->alias(); } QString Room::displayName() const { return d->displayname; } void Room::refreshDisplayName() { d->updateDisplayname(); } QString Room::topic() const { return d->getCurrentState()->topic(); } QString Room::avatarMediaId() const { return d->avatar.mediaId(); } QUrl Room::avatarUrl() const { return d->avatar.url(); } const Avatar& Room::avatarObject() const { return d->avatar; } QImage Room::avatar(int dimension) { return avatar(dimension, dimension); } QImage Room::avatar(int width, int height) { if (!d->avatar.url().isEmpty()) return d->avatar.get(connection(), width, height, [=] { emit avatarChanged(); }); // Use the first (excluding self) user's avatar for direct chats const auto dcUsers = directChatUsers(); for (auto* u : dcUsers) if (u != localUser()) return u->avatar(width, height, this, [=] { emit avatarChanged(); }); return {}; } User* Room::user(const QString& userId) const { return connection()->user(userId); } JoinState Room::memberJoinState(User* user) const { return d->membersMap.contains(user->name(this), user) ? JoinState::Join : JoinState::Leave; } JoinState Room::joinState() const { return d->joinState; } void Room::setJoinState(JoinState state) { JoinState oldState = d->joinState; if (state == oldState) return; d->joinState = state; qCDebug(STATE) << "Room" << id() << "changed state: " << int(oldState) << "->" << int(state); emit changed(Change::JoinStateChange); emit joinStateChanged(oldState, state); } Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) { auto& storedId = lastReadEventIds[u]; if (storedId == eventId) return Change::NoChange; eventIdReadUsers.remove(storedId, u); eventIdReadUsers.insert(eventId, u); swap(storedId, eventId); emit q->lastReadEventChanged(u); emit q->readMarkerForUserMoved(u, eventId, storedId); if (isLocalUser(u)) { if (storedId != serverReadMarker) connection->callApi(id, storedId); emit q->readMarkerMoved(eventId, storedId); return Change::ReadMarkerChange; } return Change::NoChange; } void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) { Q_ASSERT(from >= timeline.crbegin() && from <= timeline.crend()); Q_ASSERT(to >= from && to <= timeline.crend()); // Catch a special case when the last read event id refers to an event // that has just arrived. In this case we should recalculate // unreadMessages and might need to promote the read marker further // over local-origin messages. const auto readMarker = q->readMarker(); if (readMarker >= from && readMarker < to) { promoteReadMarker(q->localUser(), readMarker, true); return; } Q_ASSERT(to <= readMarker); QElapsedTimer et; et.start(); const auto newUnreadMessages = count_if(from, to, std::bind(&Room::Private::isEventNotable, this, _1)); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Counting gained unread messages took" << et; if (newUnreadMessages > 0) { // See https://github.com/quotient-im/libQuotient/wiki/unread_count if (unreadMessages < 0) unreadMessages = 0; unreadMessages += newUnreadMessages; qCDebug(MESSAGES) << "Room" << q->objectName() << "has gained" << newUnreadMessages << "unread message(s)," << (q->readMarker() == timeline.crend() ? "in total at least" : "in total") << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); } } Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) { Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr"); Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend()); const auto prevMarker = q->readMarker(u); if (!force && prevMarker <= newMarker) // Remember, we deal with reverse // iterators return Change::NoChange; Q_ASSERT(newMarker < timeline.crend()); // Try to auto-promote the read marker over the user's own messages // (switch to direct iterators for that). auto eagerMarker = find_if(newMarker.base(), timeline.cend(), [=](const TimelineItem& ti) { return ti->senderId() != u->id(); }); auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id()); if (isLocalUser(u)) { const auto oldUnreadCount = unreadMessages; QElapsedTimer et; et.start(); unreadMessages = int(count_if(eagerMarker, timeline.cend(), std::bind(&Room::Private::isEventNotable, this, _1))); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Recounting unread messages took" << et; // See https://github.com/quotient-im/libQuotient/wiki/unread_count if (unreadMessages == 0) unreadMessages = -1; if (force || unreadMessages != oldUnreadCount) { if (unreadMessages == -1) { qCDebug(MESSAGES) << "Room" << displayname << "has no more unread messages"; } else qCDebug(MESSAGES) << "Room" << displayname << "still has" << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); changes |= Change::UnreadNotifsChange; } } return changes; } Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker) { const auto prevMarker = q->readMarker(); auto changes = promoteReadMarker(q->localUser(), upToMarker); if (prevMarker != upToMarker) qCDebug(MESSAGES) << "Marked messages as read until" << *q->readMarker(); // We shouldn't send read receipts for the local user's own messages - so // search earlier messages for the latest message not from the local user // until the previous last-read message, whichever comes first. for (; upToMarker < prevMarker; ++upToMarker) { if ((*upToMarker)->senderId() != q->localUser()->id()) { connection->callApi(id, QStringLiteral("m.read"), QUrl::toPercentEncoding( (*upToMarker)->id())); break; } } return changes; } void Room::markMessagesAsRead(QString uptoEventId) { d->markMessagesAsRead(findInTimeline(uptoEventId)); } void Room::markAllMessagesAsRead() { if (!d->timeline.empty()) d->markMessagesAsRead(d->timeline.crbegin()); } bool Room::canSwitchVersions() const { if (!successorId().isEmpty()) return false; // No one can upgrade a room that's already upgraded // TODO, #276: m.room.power_levels const auto* plEvt = d->currentState.value({ QStringLiteral("m.room.power_levels"), {} }); if (!plEvt) return true; const auto plJson = plEvt->contentJson(); const auto currentUserLevel = plJson.value("users"_ls) .toObject() .value(localUser()->id()) .toInt(plJson.value("users_default"_ls).toInt()); const auto tombstonePowerLevel = plJson.value("events"_ls) .toObject() .value("m.room.tombstone"_ls) .toInt(plJson.value("state_default"_ls).toInt()); return currentUserLevel >= tombstonePowerLevel; } bool Room::hasUnreadMessages() const { return unreadCount() >= 0; } int Room::unreadCount() const { return d->unreadMessages; } Room::rev_iter_t Room::historyEdge() const { return d->timeline.crend(); } Room::Timeline::const_iterator Room::syncEdge() const { return d->timeline.cend(); } Room::rev_iter_t Room::timelineEdge() const { return historyEdge(); } TimelineItem::index_t Room::minTimelineIndex() const { return d->timeline.empty() ? 0 : d->timeline.front().index(); } TimelineItem::index_t Room::maxTimelineIndex() const { return d->timeline.empty() ? 0 : d->timeline.back().index(); } bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const { return !d->timeline.empty() && timelineIndex >= minTimelineIndex() && timelineIndex <= maxTimelineIndex(); } Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const { return timelineEdge() - (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0); } Room::rev_iter_t Room::findInTimeline(const QString& evtId) const { if (!d->timeline.empty() && d->eventsIndex.contains(evtId)) { auto it = findInTimeline(d->eventsIndex.value(evtId)); Q_ASSERT(it != historyEdge() && (*it)->id() == evtId); return it; } return historyEdge(); } Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId) { return std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), [txnId](const auto& item) { return item->transactionId() == txnId; }); } Room::PendingEvents::const_iterator Room::findPendingEvent(const QString& txnId) const { return std::find_if(d->unsyncedEvents.cbegin(), d->unsyncedEvents.cend(), [txnId](const auto& item) { return item->transactionId() == txnId; }); } const Room::RelatedEvents Room::relatedEvents(const QString& evtId, const char* relType) const { return d->relations.value({ evtId, relType }); } const Room::RelatedEvents Room::relatedEvents(const RoomEvent& evt, const char* relType) const { return relatedEvents(evt.id(), relType); } void Room::Private::getAllMembers() { // If already loaded or already loading, there's nothing to do here. if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob)) return; allMembersJob = connection->callApi( id, connection->nextBatchToken(), "join"); auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1; connect(allMembersJob, &BaseJob::success, q, [=] { Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1); auto roomChanges = updateStateFrom(allMembersJob->chunk()); // Replay member events that arrived after the point for which // the full members list was requested. if (!timeline.empty()) for (auto it = q->findInTimeline(nextIndex).base(); it != timeline.cend(); ++it) if (is(**it)) roomChanges |= q->processStateEvent(**it); if (roomChanges & MembersChange) emit q->memberListChanged(); emit q->allMembersLoaded(); }); } bool Room::displayed() const { return d->displayed; } void Room::setDisplayed(bool displayed) { if (d->displayed == displayed) return; d->displayed = displayed; emit displayedChanged(displayed); if (displayed) { resetHighlightCount(); resetNotificationCount(); d->getAllMembers(); } } QString Room::firstDisplayedEventId() const { return d->firstDisplayedEventId; } Room::rev_iter_t Room::firstDisplayedMarker() const { return findInTimeline(firstDisplayedEventId()); } void Room::setFirstDisplayedEventId(const QString& eventId) { if (d->firstDisplayedEventId == eventId) return; d->firstDisplayedEventId = eventId; emit firstDisplayedEventChanged(); } void Room::setFirstDisplayedEvent(TimelineItem::index_t index) { Q_ASSERT(isValidIndex(index)); setFirstDisplayedEventId(findInTimeline(index)->event()->id()); } QString Room::lastDisplayedEventId() const { return d->lastDisplayedEventId; } Room::rev_iter_t Room::lastDisplayedMarker() const { return findInTimeline(lastDisplayedEventId()); } void Room::setLastDisplayedEventId(const QString& eventId) { if (d->lastDisplayedEventId == eventId) return; d->lastDisplayedEventId = eventId; emit lastDisplayedEventChanged(); } void Room::setLastDisplayedEvent(TimelineItem::index_t index) { Q_ASSERT(isValidIndex(index)); setLastDisplayedEventId(findInTimeline(index)->event()->id()); } Room::rev_iter_t Room::readMarker(const User* user) const { Q_ASSERT(user); return findInTimeline(d->lastReadEventIds.value(user)); } Room::rev_iter_t Room::readMarker() const { return readMarker(localUser()); } QString Room::readMarkerEventId() const { return d->lastReadEventIds.value(localUser()); } QList Room::usersAtEventId(const QString& eventId) { return d->eventIdReadUsers.values(eventId); } int Room::notificationCount() const { return d->notificationCount; } void Room::resetNotificationCount() { if (d->notificationCount == 0) return; d->notificationCount = 0; emit notificationCountChanged(); } int Room::highlightCount() const { return d->highlightCount; } void Room::resetHighlightCount() { if (d->highlightCount == 0) return; d->highlightCount = 0; emit highlightCountChanged(); } void Room::switchVersion(QString newVersion) { if (!successorId().isEmpty()) { Q_ASSERT(!successorId().isEmpty()); emit upgradeFailed(tr("The room is already upgraded")); } if (auto* job = connection()->callApi(id(), newVersion)) connect(job, &BaseJob::failure, this, [this, job] { emit upgradeFailed(job->errorString()); }); else emit upgradeFailed(tr("Couldn't initiate upgrade")); } bool Room::hasAccountData(const QString& type) const { return d->accountData.find(type) != d->accountData.end(); } const EventPtr& Room::accountData(const QString& type) const { static EventPtr NoEventPtr {}; const auto it = d->accountData.find(type); return it != d->accountData.end() ? it->second : NoEventPtr; } QStringList Room::tagNames() const { return d->tags.keys(); } TagsMap Room::tags() const { return d->tags; } TagRecord Room::tag(const QString& name) const { return d->tags.value(name); } std::pair validatedTag(QString name) { if (name.contains('.')) return { false, name }; qCWarning(MAIN) << "The tag" << name << "doesn't follow the CS API conventions"; name.prepend("u."); qCWarning(MAIN) << "Using " << name << "instead"; return { true, name }; } void Room::addTag(const QString& name, const TagRecord& record) { const auto& checkRes = validatedTag(name); if (d->tags.contains(name) || (checkRes.first && d->tags.contains(checkRes.second))) return; emit tagsAboutToChange(); d->tags.insert(checkRes.second, record); emit tagsChanged(); connection()->callApi(localUser()->id(), id(), checkRes.second, record.order); } void Room::addTag(const QString& name, float order) { addTag(name, TagRecord { order }); } void Room::removeTag(const QString& name) { if (d->tags.contains(name)) { emit tagsAboutToChange(); d->tags.remove(name); emit tagsChanged(); connection()->callApi(localUser()->id(), id(), name); } else if (!name.startsWith("u.")) removeTag("u." + name); else qCWarning(MAIN) << "Tag" << name << "on room" << objectName() << "not found, nothing to remove"; } void Room::setTags(TagsMap newTags) { d->setTags(move(newTags)); connection()->callApi( localUser()->id(), id(), TagEvent::matrixTypeId(), TagEvent(d->tags).contentJson()); } void Room::Private::setTags(TagsMap newTags) { emit q->tagsAboutToChange(); const auto keys = newTags.keys(); for (const auto& k : keys) if (const auto& checkRes = validatedTag(k); checkRes.first) { if (newTags.contains(checkRes.second)) newTags.remove(k); else newTags.insert(checkRes.second, newTags.take(k)); } tags = move(newTags); qCDebug(STATE) << "Room" << q->objectName() << "is tagged with" << q->tagNames().join(QStringLiteral(", ")); emit q->tagsChanged(); } bool Room::isFavourite() const { return d->tags.contains(FavouriteTag); } bool Room::isLowPriority() const { return d->tags.contains(LowPriorityTag); } bool Room::isServerNoticeRoom() const { return d->tags.contains(ServerNoticeTag); } bool Room::isDirectChat() const { return connection()->isDirectChat(id()); } QList Room::directChatUsers() const { return connection()->directChatUsers(this); } const RoomMessageEvent* Room::Private::getEventWithFile(const QString& eventId) const { auto evtIt = q->findInTimeline(eventId); if (evtIt != timeline.rend() && is(**evtIt)) { auto* event = evtIt->viewAs(); if (event->hasFileContent()) return event; } qCWarning(MAIN) << "No files to download in event" << eventId; return nullptr; } QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const { Q_ASSERT(event->hasFileContent()); const auto* fileInfo = event->content()->fileInfo(); QString fileName; if (!fileInfo->originalName.isEmpty()) fileName = QFileInfo(fileInfo->originalName).fileName(); else if (!event->plainBody().isEmpty()) { // Having no better options, assume that the body has // the original file URL or at least the file name. if (QUrl u { event->plainBody() }; u.isValid()) fileName = QFileInfo(u.path()).fileName(); } // Check the file name for sanity if (fileName.isEmpty() || !QTemporaryFile(fileName).open()) return "file." % fileInfo->mimeType.preferredSuffix(); if (QSysInfo::productType() == "windows") { if (const auto& suffixes = fileInfo->mimeType.suffixes(); suffixes.isEmpty() && std::none_of(suffixes.begin(), suffixes.end(), [&fileName](const QString& s) { return fileName.endsWith(s); })) return fileName % '.' % fileInfo->mimeType.preferredSuffix(); } return fileName; } QUrl Room::urlToThumbnail(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) if (event->hasThumbnail()) { auto* thumbnail = event->content()->thumbnailInfo(); Q_ASSERT(thumbnail != nullptr); return MediaThumbnailJob::makeRequestUrl(connection()->homeserver(), thumbnail->url, thumbnail->imageSize); } qCDebug(MAIN) << "Event" << eventId << "has no thumbnail"; return {}; } QUrl Room::urlToDownload(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) { auto* fileInfo = event->content()->fileInfo(); Q_ASSERT(fileInfo != nullptr); return DownloadFileJob::makeRequestUrl(connection()->homeserver(), fileInfo->url); } return {}; } QString Room::fileNameToDownload(const QString& eventId) const { if (auto* event = d->getEventWithFile(eventId)) return d->fileNameToDownload(event); return {}; } FileTransferInfo Room::fileTransferInfo(const QString& id) const { auto infoIt = d->fileTransfers.find(id); if (infoIt == d->fileTransfers.end()) return {}; // FIXME: Add lib tests to make sure FileTransferInfo::status stays // consistent with FileTransferInfo::job qint64 progress = infoIt->progress; qint64 total = infoIt->total; if (total > INT_MAX) { // JavaScript doesn't deal with 64-bit integers; scale down if necessary progress = llround(double(progress) / total * INT_MAX); total = INT_MAX; } return { infoIt->status, infoIt->isUpload, int(progress), int(total), QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()), QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()) }; } QUrl Room::fileSource(const QString& id) const { auto url = urlToDownload(id); if (url.isValid()) return url; // No urlToDownload means it's a pending or completed upload. auto infoIt = d->fileTransfers.find(id); if (infoIt != d->fileTransfers.end()) return QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); qCWarning(MAIN) << "File source for identifier" << id << "not found"; return {}; } QString Room::prettyPrint(const QString& plainText) const { return Quotient::prettyPrint(plainText); } QList Room::usersTyping() const { return d->usersTyping; } QList Room::membersLeft() const { return d->membersLeft; } QList Room::users() const { return d->membersMap.values(); } QStringList Room::memberNames() const { QStringList res; for (auto u : qAsConst(d->membersMap)) res.append(roomMembername(u)); return res; } int Room::memberCount() const { return d->membersMap.size(); } int Room::timelineSize() const { return int(d->timeline.size()); } bool Room::usesEncryption() const { return !d->getCurrentState()->algorithm().isEmpty(); } const StateEventBase* Room::getCurrentState(const QString& evtType, const QString& stateKey) const { return d->getCurrentState({ evtType, stateKey }); } RoomEventPtr Room::decryptMessage(EncryptedEvent* encryptedEvent) { if (encryptedEvent->algorithm() == OlmV1Curve25519AesSha2AlgoKey) { QString identityKey = connection()->olmAccount()->curve25519IdentityKey(); QJsonObject personalCipherObject = encryptedEvent->ciphertext(identityKey); if (personalCipherObject.isEmpty()) { qCDebug(E2EE) << "Encrypted event is not for the current device"; return {}; } return makeEvent(decryptMessage( personalCipherObject, encryptedEvent->senderKey().toLatin1())); } if (encryptedEvent->algorithm() == MegolmV1AesSha2AlgoKey) { return makeEvent(decryptMessage( encryptedEvent->ciphertext(), encryptedEvent->senderKey(), encryptedEvent->deviceId(), encryptedEvent->sessionId())); } return {}; } QString Room::decryptMessage(QJsonObject personalCipherObject, QByteArray senderKey) { QString decrypted; using namespace QtOlm; // TODO: new objects to private fields: InboundSession* session; int type = personalCipherObject.value(TypeKeyL).toInt(-1); QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); PreKeyMessage preKeyMessage { body }; session = new InboundSession(connection()->olmAccount(), &preKeyMessage, senderKey, this); if (type == 0) { if (!session->matches(&preKeyMessage, senderKey)) { connection()->olmAccount()->removeOneTimeKeys(session); } try { decrypted = session->decrypt(&preKeyMessage); } catch (std::runtime_error& e) { qCWarning(EVENTS) << "Decrypt failed:" << e.what(); } } else if (type == 1) { Message message { body }; if (!session->matches(&preKeyMessage, senderKey)) { qCWarning(EVENTS) << "Invalid encrypted message"; } try { decrypted = session->decrypt(&message); } catch (std::runtime_error& e) { qCWarning(EVENTS) << "Decrypt failed:" << e.what(); } } return decrypted; } QString Room::sessionKey(const QString& senderKey, const QString& deviceId, const QString& sessionId) const { // TODO: handling an m.room_key event return ""; } QString Room::decryptMessage(QByteArray cipher, const QString& senderKey, const QString& deviceId, const QString& sessionId) { QString decrypted; using namespace QtOlm; InboundGroupSession* groupSession; groupSession = new InboundGroupSession( sessionKey(senderKey, deviceId, sessionId).toLatin1()); groupSession->decrypt(cipher); // TODO: avoid replay attacks return decrypted; } int Room::joinedCount() const { return d->summary.joinedMemberCount.omitted() ? d->membersMap.size() : d->summary.joinedMemberCount.value(); } int Room::invitedCount() const { // TODO: Store invited users in Room too Q_ASSERT(!d->summary.invitedMemberCount.omitted()); return d->summary.invitedMemberCount.value(); } int Room::totalMemberCount() const { return joinedCount() + invitedCount(); } GetRoomEventsJob* Room::eventsHistoryJob() const { return d->eventsHistoryJob; } Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) { if (!summary.merge(newSummary)) return Change::NoChange; qCDebug(MAIN).nospace().noquote() << "Updated room summary for " << q->objectName() << ": " << summary; emit q->memberListChanged(); return Change::SummaryChange; } void Room::Private::insertMemberIntoMap(User* u) { const auto userName = u->name(q); // If there is exactly one namesake of the added user, signal member // renaming for that other one because the two should be disambiguated now. const auto namesakes = membersMap.values(userName); // Callers should check they are not adding an existing user once more. Q_ASSERT(!namesakes.contains(u)); if (namesakes.size() == 1) emit q->memberAboutToRename(namesakes.front(), namesakes.front()->fullName(q)); membersMap.insert(userName, u); if (namesakes.size() == 1) emit q->memberRenamed(namesakes.front()); } void Room::Private::renameMember(User* u, const QString& oldName) { if (u->name(q) == oldName) { qCWarning(MAIN) << "Room::Private::renameMember(): the user " << u->fullName(q) << "is already known in the room under a new name."; } else if (membersMap.contains(oldName, u)) { removeMemberFromMap(oldName, u); insertMemberIntoMap(u); } } void Room::Private::removeMemberFromMap(const QString& username, User* u) { User* namesake = nullptr; auto namesakes = membersMap.values(username); if (namesakes.size() == 2) { namesake = namesakes.front() == u ? namesakes.back() : namesakes.front(); Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken"); emit q->memberAboutToRename(namesake, username); } membersMap.remove(username, u); // If there was one namesake besides the removed user, signal member // renaming for it because it doesn't need to be disambiguated any more. if (namesake) emit q->memberRenamed(namesake); } inline auto makeErrorStr(const Event& e, QByteArray msg) { return msg.append("; event dump follows:\n").append(e.originalJson()); } Room::Timeline::size_type Room::Private::moveEventsToTimeline(RoomEventsRange events, EventsPlacement placement) { Q_ASSERT(!events.empty()); // Historical messages arrive in newest-to-oldest order, so the process for // them is almost symmetric to the one for new messages. New messages get // appended from index 0; old messages go backwards from index -1. auto index = timeline.empty() ? -((placement + 1) / 2) /* 1 -> -1; -1 -> 0 */ : placement == Older ? timeline.front().index() : timeline.back().index(); auto baseIndex = index; for (auto&& e : events) { const auto eId = e->id(); Q_ASSERT_X(e, __FUNCTION__, "Attempt to add nullptr to timeline"); Q_ASSERT_X( !eId.isEmpty(), __FUNCTION__, makeErrorStr(*e, "Event with empty id cannot be in the timeline")); Q_ASSERT_X( !eventsIndex.contains(eId), __FUNCTION__, makeErrorStr(*e, "Event is already in the timeline; " "incoming events were not properly deduplicated")); if (placement == Older) timeline.emplace_front(move(e), --index); else timeline.emplace_back(move(e), ++index); eventsIndex.insert(eId, index); Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId); } const auto insertedSize = (index - baseIndex) * placement; Q_ASSERT(insertedSize == int(events.size())); return insertedSize; } QString Room::roomMembername(const User* u) const { // See the CS spec, section 11.2.2.3 const auto username = u->name(this); if (username.isEmpty()) return u->id(); auto namesakesIt = qAsConst(d->membersMap).find(username); // We expect a user to be a member of the room - but technically it is // possible to invoke roomMemberName() even for non-members. In such case // we return the full name, just in case. if (namesakesIt == d->membersMap.cend()) return u->fullName(this); auto nextUserIt = namesakesIt + 1; if (nextUserIt == d->membersMap.cend() || nextUserIt.key() != username) return username; // No disambiguation necessary // Check if we can get away just attaching the bridge postfix // (extension to the spec) QVector bridges; for (; namesakesIt != d->membersMap.cend() && namesakesIt.key() == username; ++namesakesIt) { const auto bridgeName = (*namesakesIt)->bridged(); if (bridges.contains(bridgeName)) // Two accounts on the same bridge return u->fullName(this); // Disambiguate fully // Don't bother sorting, not so many bridges out there bridges.push_back(bridgeName); } return u->rawName(this); // Disambiguate using the bridge postfix only } QString Room::roomMembername(const QString& userId) const { return roomMembername(user(userId)); } void Room::updateData(SyncRoomData&& data, bool fromCache) { if (d->prevBatch.isEmpty()) d->prevBatch = data.timelinePrevBatch; setJoinState(data.joinState); Changes roomChanges = Change::NoChange; QElapsedTimer et; et.start(); for (auto&& event : data.accountData) roomChanges |= processAccountDataEvent(move(event)); roomChanges |= d->updateStateFrom(data.state); if (!data.timeline.empty()) { et.restart(); roomChanges |= d->addNewMessageEvents(move(data.timeline)); if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << data.timeline.size() << "event(s)," << et; } if (roomChanges & TopicChange) emit topicChanged(); if (roomChanges & NameChange) emit namesChanged(this); if (roomChanges & MembersChange) emit memberListChanged(); roomChanges |= d->setSummary(move(data.summary)); for (auto&& ephemeralEvent : data.ephemeral) roomChanges |= processEphemeralEvent(move(ephemeralEvent)); // See https://github.com/quotient-im/libQuotient/wiki/unread_count if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) { qCDebug(MESSAGES) << "Setting unread_count to" << data.unreadCount; d->unreadMessages = data.unreadCount; roomChanges |= Change::UnreadNotifsChange; emit unreadMessagesChanged(this); } if (data.highlightCount != d->highlightCount) { d->highlightCount = data.highlightCount; roomChanges |= Change::UnreadNotifsChange; emit highlightCountChanged(); } if (data.notificationCount != d->notificationCount) { d->notificationCount = data.notificationCount; roomChanges |= Change::UnreadNotifsChange; emit notificationCountChanged(); } if (roomChanges != Change::NoChange) { d->updateDisplayname(); emit changed(roomChanges); if (!fromCache) connection()->saveRoomState(this); } } RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) { if (event->transactionId().isEmpty()) event->setTransactionId(connection->generateTxnId()); if (event->roomId().isEmpty()) event->setRoomId(id); if (event->senderId().isEmpty()) event->setSender(connection->userId()); auto* pEvent = rawPtr(event); emit q->pendingEventAboutToAdd(pEvent); unsyncedEvents.emplace_back(move(event)); emit q->pendingEventAdded(); return pEvent; } QString Room::Private::sendEvent(RoomEventPtr&& event) { if (q->usesEncryption()) { qCCritical(MAIN) << "Room" << q->objectName() << "enforces encryption; sending encrypted messages " "is not supported yet"; } if (q->successorId().isEmpty()) return doSendEvent(addAsPending(std::move(event))); qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; return {}; } QString Room::Private::doSendEvent(const RoomEvent* pEvent) { const auto txnId = pEvent->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. if (auto call = connection->callApi(BackgroundRequest, id, pEvent->matrixType(), txnId, pEvent->contentJson())) { Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] { auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { qCWarning(EVENTS) << "Pending event for transaction" << txnId << "not found - got synced so soon?"; return; } it->setDeparted(); qCDebug(EVENTS) << "Event txn" << txnId << "has departed"; emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); }); Room::connect(call, &BaseJob::failure, q, std::bind(&Room::Private::onEventSendingFailure, this, txnId, call)); Room::connect(call, &BaseJob::success, q, [this, call, txnId] { emit q->messageSent(txnId, call->eventId()); auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { qCDebug(EVENTS) << "Pending event for transaction" << txnId << "already merged"; return; } if (it->deliveryStatus() != EventStatus::ReachedServer) { it->setReachedServer(call->eventId()); emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); } }); } else onEventSendingFailure(txnId); return txnId; } void Room::Private::onEventSendingFailure(const QString& txnId, BaseJob* call) { auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { qCritical(EVENTS) << "Pending event for transaction" << txnId << "could not be sent"; return; } it->setSendingFailed(call ? call->statusCaption() % ": " % call->errorString() : tr("The call could not be started")); emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); } QString Room::retryMessage(const QString& txnId) { const auto it = findPendingEvent(txnId); Q_ASSERT(it != d->unsyncedEvents.end()); qCDebug(EVENTS) << "Retrying transaction" << txnId; const auto& transferIt = d->fileTransfers.find(txnId); if (transferIt != d->fileTransfers.end()) { Q_ASSERT(transferIt->isUpload); if (transferIt->status == FileTransferInfo::Completed) { qCDebug(MESSAGES) << "File for transaction" << txnId << "has already been uploaded, bypassing re-upload"; } else { if (isJobRunning(transferIt->job)) { qCDebug(MESSAGES) << "Abandoning the upload job for transaction" << txnId << "and starting again"; transferIt->job->abandon(); emit fileTransferFailed(txnId, tr("File upload will be retried")); } uploadFile(txnId, QUrl::fromLocalFile( transferIt->localFileInfo.absoluteFilePath())); // FIXME: Content type is no more passed here but it should } } if (it->deliveryStatus() == EventStatus::ReachedServer) { qCWarning(MAIN) << "The previous attempt has reached the server; two" " events are likely to be in the timeline after retry"; } it->resetStatus(); emit pendingEventChanged(int(it - d->unsyncedEvents.begin())); return d->doSendEvent(it->event()); } void Room::discardMessage(const QString& txnId) { auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), [txnId](const auto& evt) { return evt->transactionId() == txnId; }); Q_ASSERT(it != d->unsyncedEvents.end()); qCDebug(EVENTS) << "Discarding transaction" << txnId; const auto& transferIt = d->fileTransfers.find(txnId); if (transferIt != d->fileTransfers.end()) { Q_ASSERT(transferIt->isUpload); if (isJobRunning(transferIt->job)) { transferIt->status = FileTransferInfo::Cancelled; transferIt->job->abandon(); emit fileTransferFailed(txnId, tr("File upload cancelled")); } else if (transferIt->status == FileTransferInfo::Completed) { qCWarning(MAIN) << "File for transaction" << txnId << "has been uploaded but the message was discarded"; } } emit pendingEventAboutToDiscard(int(it - d->unsyncedEvents.begin())); d->unsyncedEvents.erase(it); emit pendingEventDiscarded(); } QString Room::postMessage(const QString& plainText, MessageEventType type) { return d->sendEvent(plainText, type); } QString Room::postPlainText(const QString& plainText) { return postMessage(plainText, MessageEventType::Text); } QString Room::postHtmlMessage(const QString& plainText, const QString& html, MessageEventType type) { return d->sendEvent( plainText, type, new EventContent::TextContent(html, QStringLiteral("text/html"))); } QString Room::postHtmlText(const QString& plainText, const QString& html) { return postHtmlMessage(plainText, html); } QString Room::postReaction(const QString& eventId, const QString& key) { return d->sendEvent(EventRelation::annotate(eventId, key)); } QString Room::postFile(const QString& plainText, const QUrl& localPath, bool asGenericFile) { QFileInfo localFile { localPath.toLocalFile() }; Q_ASSERT(localFile.isFile()); const auto txnId = connection()->generateTxnId(); // Remote URL will only be known after upload; fill in the local path // to enable the preview while the event is pending. uploadFile(txnId, localPath); { auto&& event = makeEvent(plainText, localFile, asGenericFile); event->setTransactionId(txnId); d->addAsPending(std::move(event)); } auto* context = new QObject(this); connect(this, &Room::fileTransferCompleted, context, [context, this, txnId](const QString& id, QUrl, const QUrl& mxcUri) { if (id == txnId) { auto it = findPendingEvent(txnId); if (it != d->unsyncedEvents.end()) { it->setFileUploaded(mxcUri); emit pendingEventChanged( int(it - d->unsyncedEvents.begin())); d->doSendEvent(it->get()); } else { // Normally in this situation we should instruct // the media server to delete the file; alas, there's no // API specced for that. qCWarning(MAIN) << "File uploaded to" << mxcUri << "but the event referring to it was " "cancelled"; } context->deleteLater(); } }); connect(this, &Room::fileTransferCancelled, this, [context, this, txnId](const QString& id) { if (id == txnId) { auto it = findPendingEvent(txnId); if (it != d->unsyncedEvents.end()) { const auto idx = int(it - d->unsyncedEvents.begin()); emit pendingEventAboutToDiscard(idx); // See #286 on why iterator may not be valid here. d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx); emit pendingEventDiscarded(); } context->deleteLater(); } }); return txnId; } QString Room::postEvent(RoomEvent* event) { return d->sendEvent(RoomEventPtr(event)); } QString Room::postJson(const QString& matrixType, const QJsonObject& eventContent) { return d->sendEvent(loadEvent(matrixType, eventContent)); } SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) const { return d->requestSetState(evt); } void Room::setName(const QString& newName) { d->requestSetState(newName); } void Room::setCanonicalAlias(const QString& newAlias) { d->requestSetState(newAlias); } void Room::setLocalAliases(const QStringList& aliases) { d->requestSetState(connection()->homeserver().authority(), aliases); } void Room::setTopic(const QString& newTopic) { d->requestSetState(newTopic); } bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) { if (le->type() != re->type()) return false; if (!re->id().isEmpty()) return le->id() == re->id(); if (!re->transactionId().isEmpty()) return le->transactionId() == re->transactionId(); // This one is not reliable (there can be two unsynced // events with the same type, sender and state key) but // it's the best we have for state events. if (re->isStateEvent()) return le->stateKey() == re->stateKey(); // Empty id and no state key, hmm... (shrug) return le->contentJson() == re->contentJson(); } bool Room::supportsCalls() const { return joinedCount() == 2; } void Room::checkVersion() { const auto defaultVersion = connection()->defaultRoomVersion(); const auto stableVersions = connection()->stableRoomVersions(); Q_ASSERT(!defaultVersion.isEmpty()); // This method is only called after the base state has been loaded // or the server capabilities have been loaded. emit stabilityUpdated(defaultVersion, stableVersions); if (!stableVersions.contains(version())) { qCDebug(MAIN) << this << "version is" << version() << "which the server doesn't count as stable"; if (canSwitchVersions()) qCDebug(MAIN) << "The current user has enough privileges to fix it"; } } void Room::inviteCall(const QString& callId, const int lifetime, const QString& sdp) { Q_ASSERT(supportsCalls()); d->sendEvent(callId, lifetime, sdp); } void Room::sendCallCandidates(const QString& callId, const QJsonArray& candidates) { Q_ASSERT(supportsCalls()); d->sendEvent(callId, candidates); } void Room::answerCall(const QString& callId, const int lifetime, const QString& sdp) { Q_ASSERT(supportsCalls()); d->sendEvent(callId, lifetime, sdp); } void Room::answerCall(const QString& callId, const QString& sdp) { Q_ASSERT(supportsCalls()); d->sendEvent(callId, sdp); } void Room::hangupCall(const QString& callId) { Q_ASSERT(supportsCalls()); d->sendEvent(callId); } void Room::getPreviousContent(int limit) { d->getPreviousContent(limit); } void Room::Private::getPreviousContent(int limit) { if (isJobRunning(eventsHistoryJob)) return; eventsHistoryJob = connection->callApi(id, prevBatch, "b", "", limit); emit q->eventsHistoryJobChanged(); connect(eventsHistoryJob, &BaseJob::success, q, [=] { prevBatch = eventsHistoryJob->end(); addHistoricalMessageEvents(eventsHistoryJob->chunk()); }); connect(eventsHistoryJob, &QObject::destroyed, q, &Room::eventsHistoryJobChanged); } void Room::inviteToRoom(const QString& memberId) { connection()->callApi(id(), memberId); } LeaveRoomJob* Room::leaveRoom() { // FIXME, #63: It should be RoomManager, not Connection return connection()->leaveRoom(this); } SetRoomStateWithKeyJob* Room::setMemberState(const QString& memberId, const RoomMemberEvent& event) const { return d->requestSetState(memberId, event.content()); } void Room::kickMember(const QString& memberId, const QString& reason) { connection()->callApi(id(), memberId, reason); } void Room::ban(const QString& userId, const QString& reason) { connection()->callApi(id(), userId, reason); } void Room::unban(const QString& userId) { connection()->callApi(id(), userId); } void Room::redactEvent(const QString& eventId, const QString& reason) { connection()->callApi(id(), QUrl::toPercentEncoding(eventId), connection()->generateTxnId(), reason); } void Room::uploadFile(const QString& id, const QUrl& localFilename, const QString& overrideContentType) { Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__, "localFilename should point at a local file"); auto fileName = localFilename.toLocalFile(); auto job = connection()->uploadFile(fileName, overrideContentType); if (isJobRunning(job)) { d->fileTransfers.insert(id, { job, fileName, true }); connect(job, &BaseJob::uploadProgress, this, [this, id](qint64 sent, qint64 total) { d->fileTransfers[id].update(sent, total); emit fileTransferProgress(id, sent, total); }); connect(job, &BaseJob::success, this, [this, id, localFilename, job] { d->fileTransfers[id].status = FileTransferInfo::Completed; emit fileTransferCompleted(id, localFilename, job->contentUri()); }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, id, job->errorString())); emit newFileTransfer(id, localFilename); } else d->failedTransfer(id); } void Room::downloadFile(const QString& eventId, const QUrl& localFilename) { auto ongoingTransfer = d->fileTransfers.find(eventId); if (ongoingTransfer != d->fileTransfers.end() && ongoingTransfer->status == FileTransferInfo::Started) { qCWarning(MAIN) << "Transfer for" << eventId << "is ongoing; download won't start"; return; } Q_ASSERT_X(localFilename.isEmpty() || localFilename.isLocalFile(), __FUNCTION__, "localFilename should point at a local file"); const auto* event = d->getEventWithFile(eventId); if (!event) { qCCritical(MAIN) << eventId << "is not in the local timeline or has no file content"; Q_ASSERT(false); return; } const auto* const fileInfo = event->content()->fileInfo(); if (!fileInfo->isValid()) { qCWarning(MAIN) << "Event" << eventId << "has an empty or malformed mxc URL; won't download"; return; } const auto fileUrl = fileInfo->url; auto filePath = localFilename.toLocalFile(); if (filePath.isEmpty()) { // Build our own file path, starting with temp directory and eventId. filePath = eventId; filePath = QDir::tempPath() % '/' % filePath.replace(QRegularExpression("[/\\<>|\"*?:]"), "_") % '#' % d->fileNameToDownload(event); } auto job = connection()->downloadFile(fileUrl, filePath); if (isJobRunning(job)) { // If there was a previous transfer (completed or failed), remove it. d->fileTransfers.remove(eventId); d->fileTransfers.insert(eventId, { job, job->targetFileName() }); connect(job, &BaseJob::downloadProgress, this, [this, eventId](qint64 received, qint64 total) { d->fileTransfers[eventId].update(received, total); emit fileTransferProgress(eventId, received, total); }); connect(job, &BaseJob::success, this, [this, eventId, fileUrl, job] { d->fileTransfers[eventId].status = FileTransferInfo::Completed; emit fileTransferCompleted( eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName())); }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, eventId, job->errorString())); } else d->failedTransfer(eventId); } void Room::cancelFileTransfer(const QString& id) { auto it = d->fileTransfers.find(id); if (it == d->fileTransfers.end()) { qCWarning(MAIN) << "No information on file transfer" << id << "in room" << d->id; return; } if (isJobRunning(it->job)) it->job->abandon(); d->fileTransfers.remove(id); emit fileTransferCancelled(id); } void Room::Private::dropDuplicateEvents(RoomEvents& events) const { if (events.empty()) return; // Multiple-remove (by different criteria), single-erase // 1. Check for duplicates against the timeline. auto dupsBegin = remove_if(events.begin(), events.end(), [&](const RoomEventPtr& e) { return eventsIndex.contains(e->id()); }); // 2. Check for duplicates within the batch if there are still events. for (auto eIt = events.begin(); distance(eIt, dupsBegin) > 1; ++eIt) dupsBegin = remove_if(eIt + 1, dupsBegin, [&](const RoomEventPtr& e) { return e->id() == (*eIt)->id(); }); if (dupsBegin == events.end()) return; qCDebug(EVENTS) << "Dropping" << distance(dupsBegin, events.end()) << "duplicate event(s)"; events.erase(dupsBegin, events.end()); } /** Make a redacted event * * This applies the redaction procedure as defined by the CS API specification * to the event's JSON and returns the resulting new event. It is * the responsibility of the caller to dispose of the original event after that. */ RoomEventPtr makeRedacted(const RoomEvent& target, const RedactionEvent& redaction) { auto originalJson = target.originalJsonObject(); static const QStringList keepKeys { EventIdKey, TypeKey, QStringLiteral("room_id"), QStringLiteral("sender"), StateKeyKey, QStringLiteral("prev_content"), ContentKey, QStringLiteral("hashes"), QStringLiteral("signatures"), QStringLiteral("depth"), QStringLiteral("prev_events"), QStringLiteral("prev_state"), QStringLiteral("auth_events"), QStringLiteral("origin"), QStringLiteral("origin_server_ts"), QStringLiteral("membership") }; std::vector> keepContentKeysMap { { RoomMemberEvent::typeId(), { QStringLiteral("membership") } }, { RoomCreateEvent::typeId(), { QStringLiteral("creator") } } // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } // , { RoomPowerLevels::typeId(), // { QStringLiteral("ban"), QStringLiteral("events"), // QStringLiteral("events_default"), // QStringLiteral("kick"), QStringLiteral("redact"), // QStringLiteral("state_default"), QStringLiteral("users"), // QStringLiteral("users_default") } } , { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } } // , { RoomHistoryVisibility::typeId(), // { QStringLiteral("history_visibility") } } }; for (auto it = originalJson.begin(); it != originalJson.end();) { if (!keepKeys.contains(it.key())) it = originalJson.erase(it); // TODO: shred the value else ++it; } auto keepContentKeys = find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(), [&target](const auto& t) { return target.type() == t.first; }); if (keepContentKeys == keepContentKeysMap.end()) { originalJson.remove(ContentKeyL); originalJson.remove(PrevContentKeyL); } else { auto content = originalJson.take(ContentKeyL).toObject(); for (auto it = content.begin(); it != content.end();) { if (!keepContentKeys->second.contains(it.key())) it = content.erase(it); else ++it; } originalJson.insert(ContentKey, content); } auto unsignedData = originalJson.take(UnsignedKeyL).toObject(); unsignedData[RedactedCauseKeyL] = redaction.originalJsonObject(); originalJson.insert(QStringLiteral("unsigned"), unsignedData); return loadEvent(originalJson); } bool Room::Private::processRedaction(const RedactionEvent& redaction) { // Can't use findInTimeline because it returns a const iterator, and // we need to change the underlying TimelineItem. const auto pIdx = eventsIndex.find(redaction.redactedEvent()); if (pIdx == eventsIndex.end()) return false; Q_ASSERT(q->isValidIndex(*pIdx)); auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; if (ti->isRedacted() && ti->redactedBecause()->id() == redaction.id()) { qCDebug(EVENTS) << "Redaction" << redaction.id() << "of event" << ti->id() << "already done, skipping"; return true; } // Make a new event from the redacted JSON and put it in the timeline // instead of the redacted one. oldEvent will be deleted on return. auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction)); qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id(); if (oldEvent->isStateEvent()) { const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() }; Q_ASSERT(currentState.contains(evtKey)); if (currentState.value(evtKey) == oldEvent.get()) { Q_ASSERT(ti.index() >= 0); // Historical states can't be in // currentState qCDebug(EVENTS).nospace() << "Redacting state " << oldEvent->matrixType() << "/" << oldEvent->stateKey(); // Retarget the current state to the newly made event. if (q->processStateEvent(*ti)) emit q->namesChanged(q); updateDisplayname(); } } if (const auto* reaction = eventCast(oldEvent)) { const auto& targetEvtId = reaction->relation().eventId; const auto lookupKey = qMakePair(targetEvtId, EventRelation::Annotation()); if (relations.contains(lookupKey)) { relations[lookupKey].removeOne(reaction); } } q->onRedaction(*oldEvent, *ti); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); return true; } /** Make a replaced event * * Takes \p target and returns a copy of it with content taken from * \p replacement. Disposal of the original event after that is on the caller. */ RoomEventPtr makeReplaced(const RoomEvent& target, const RoomMessageEvent& replacement) { auto originalJson = target.originalJsonObject(); originalJson[ContentKeyL] = replacement.contentJson(); auto unsignedData = originalJson.take(UnsignedKeyL).toObject(); auto relations = unsignedData.take("m.relations"_ls).toObject(); relations["m.replace"_ls] = replacement.id(); unsignedData.insert(QStringLiteral("m.relations"), relations); originalJson.insert(UnsignedKey, unsignedData); return loadEvent(originalJson); } bool Room::Private::processReplacement(const RoomMessageEvent& newEvent) { // Can't use findInTimeline because it returns a const iterator, and // we need to change the underlying TimelineItem. const auto pIdx = eventsIndex.find(newEvent.replacedEvent()); if (pIdx == eventsIndex.end()) return false; Q_ASSERT(q->isValidIndex(*pIdx)); auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; if (ti->replacedBy() == newEvent.id()) { qCDebug(EVENTS) << "Event" << ti->id() << "is already replaced with" << newEvent.id(); return true; } // Make a new event from the redacted JSON and put it in the timeline // instead of the redacted one. oldEvent will be deleted on return. auto oldEvent = ti.replaceEvent(makeReplaced(*ti, newEvent)); qCDebug(EVENTS) << "Replaced" << oldEvent->id() << "with" << newEvent.id(); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); return true; } Connection* Room::connection() const { Q_ASSERT(d->connection); return d->connection; } User* Room::localUser() const { return connection()->user(); } /// Whether the event is a redaction or a replacement inline bool isEditing(const RoomEventPtr& ep) { Q_ASSERT(ep); if (is(*ep)) return true; if (auto* msgEvent = eventCast(ep)) return msgEvent->replacedEvent().isEmpty(); return false; } Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) { dropDuplicateEvents(events); if (events.empty()) return Change::NoChange; { // Pre-process redactions and edits so that events that get // redacted/replaced in the same batch landed in the timeline already // treated. // NB: We have to store redacting/replacing events to the timeline too - // see #220. auto it = std::find_if(events.begin(), events.end(), isEditing); for (const auto& eptr : RoomEventsRange(it, events.end())) { if (auto* r = eventCast(eptr)) { // Try to find the target in the timeline, then in the batch. if (processRedaction(*r)) continue; auto targetIt = std::find_if(events.begin(), it, [id = r->redactedEvent()]( const RoomEventPtr& ep) { return ep->id() == id; }); if (targetIt != it) *targetIt = makeRedacted(**targetIt, *r); else qCDebug(EVENTS) << "Redaction" << r->id() << "ignored: target event" << r->redactedEvent() << "is not found"; // If the target event comes later, it comes already redacted. } if (auto* msg = eventCast(eptr)) { if (!msg->replacedEvent().isEmpty()) { if (processReplacement(*msg)) continue; auto targetIt = std::find_if(events.begin(), it, [id = msg->replacedEvent()]( const RoomEventPtr& ep) { return ep->id() == id; }); if (targetIt != it) *targetIt = makeReplaced(**targetIt, *msg); else // FIXME: don't ignore, just show it wherever it arrived qCDebug(EVENTS) << "Replacing event" << msg->id() << "ignored: replaced event" << msg->replacedEvent() << "is not found"; // Same as with redactions above, the replaced event coming // later will come already with the new content. } } } } // State changes arrive as a part of timeline; the current room state gets // updated before merging events to the timeline because that's what // clients historically expect. This may eventually change though if we // postulate that the current state is only current between syncs but not // within a sync. Changes roomChanges = Change::NoChange; for (const auto& eptr : events) roomChanges |= q->processStateEvent(*eptr); auto timelineSize = timeline.size(); size_t totalInserted = 0; for (auto it = events.begin(); it != events.end();) { auto nextPendingPair = findFirstOf(it, events.end(), unsyncedEvents.begin(), unsyncedEvents.end(), isEchoEvent); const auto& remoteEcho = nextPendingPair.first; const auto& localEcho = nextPendingPair.second; if (it != remoteEcho) { RoomEventsRange eventsSpan { it, remoteEcho }; emit q->aboutToAddNewMessages(eventsSpan); auto insertedSize = moveEventsToTimeline(eventsSpan, Newer); totalInserted += insertedSize; auto firstInserted = timeline.cend() - insertedSize; q->onAddNewTimelineEvents(firstInserted); emit q->addedMessages(firstInserted->index(), timeline.back().index()); } if (remoteEcho == events.end()) break; it = remoteEcho + 1; auto* nextPendingEvt = remoteEcho->get(); const auto pendingEvtIdx = int(localEcho - unsyncedEvents.begin()); if (localEcho->deliveryStatus() != EventStatus::ReachedServer) { localEcho->setReachedServer(nextPendingEvt->id()); emit q->pendingEventChanged(pendingEvtIdx); } emit q->pendingEventAboutToMerge(nextPendingEvt, pendingEvtIdx); qCDebug(MESSAGES) << "Merging pending event from transaction" << nextPendingEvt->transactionId() << "into" << nextPendingEvt->id(); auto transfer = fileTransfers.take(nextPendingEvt->transactionId()); if (transfer.status != FileTransferInfo::None) fileTransfers.insert(nextPendingEvt->id(), transfer); // After emitting pendingEventAboutToMerge() above we cannot rely // on the previously obtained localEcho staying valid // because a signal handler may send another message, thereby altering // unsyncedEvents (see #286). Fortunately, unsyncedEvents only grows at // its back so we can rely on the index staying valid at least. unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx); if (auto insertedSize = moveEventsToTimeline({ remoteEcho, it }, Newer)) { totalInserted += insertedSize; q->onAddNewTimelineEvents(timeline.cend() - insertedSize); } emit q->pendingEventMerged(); } // Events merged and transferred from `events` to `timeline` now. const auto from = timeline.cend() - totalInserted; if (q->supportsCalls()) for (auto it = from; it != timeline.cend(); ++it) if (auto* evt = it->viewAs()) emit q->callEvent(q, evt); if (totalInserted > 0) { for (auto it = from; it != timeline.cend(); ++it) { if (const auto* reaction = it->viewAs()) { const auto& relation = reaction->relation(); relations[{ relation.eventId, relation.type }] << reaction; emit q->updatedEvent(relation.eventId); } } qCDebug(MESSAGES) << "Room" << q->objectName() << "received" << totalInserted << "new events; the last event is now" << timeline.back(); // The first event in the just-added batch (referred to by `from`) // defines whose read marker can possibly be promoted any further over // the same author's events newly arrived. Others will need explicit // read receipts from the server (or, for the local user, // markMessagesAsRead() invocation) to promote their read markers over // the new message events. auto firstWriter = q->user((*from)->senderId()); if (q->readMarker(firstWriter) != timeline.crend()) { roomChanges |= promoteReadMarker(firstWriter, rev_iter_t(from) - 1); qCDebug(MESSAGES) << "Auto-promoted read marker for" << firstWriter->id() << "to" << *q->readMarker(firstWriter); } updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); roomChanges |= Change::UnreadNotifsChange; } Q_ASSERT(timeline.size() == timelineSize + totalInserted); return roomChanges; } void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) { QElapsedTimer et; et.start(); const auto timelineSize = timeline.size(); dropDuplicateEvents(events); if (events.empty()) return; // In case of lazy-loading new members may be loaded with historical // messages. Also, the cache doesn't store events with empty content; // so when such events show up in the timeline they should be properly // incorporated. for (const auto& eptr : events) { const auto& e = *eptr; if (e.isStateEvent() && !currentState.contains({ e.matrixType(), e.stateKey() })) { q->processStateEvent(e); } } emit q->aboutToAddHistoricalMessages(events); const auto insertedSize = moveEventsToTimeline(events, Older); const auto from = timeline.crend() - insertedSize; qCDebug(MESSAGES) << "Room" << displayname << "received" << insertedSize << "past events; the oldest event is now" << timeline.front(); q->onAddHistoricalTimelineEvents(from); emit q->addedMessages(timeline.front().index(), from->index()); for (auto it = from; it != timeline.crend(); ++it) { if (const auto* reaction = it->viewAs()) { const auto& relation = reaction->relation(); relations[{ relation.eventId, relation.type }] << reaction; emit q->updatedEvent(relation.eventId); } } if (from <= q->readMarker()) updateUnreadCount(from, timeline.crend()); Q_ASSERT(timeline.size() == timelineSize + insertedSize); if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** Room::addHistoricalMessageEvents():" << insertedSize << "event(s)," << et; } Room::Changes Room::processStateEvent(const RoomEvent& e) { if (!e.isStateEvent()) return Change::NoChange; const auto* oldStateEvent = std::exchange(d->currentState[{ e.matrixType(), e.stateKey() }], static_cast(&e)); Q_ASSERT(!oldStateEvent || (oldStateEvent->matrixType() == e.matrixType() && oldStateEvent->stateKey() == e.stateKey())); if (!is(e)) // Room member events are too numerous qCDebug(EVENTS) << "Room state event:" << e; // clang-format off return visit(e , [] (const RoomNameEvent&) { return NameChange; } , [this,oldStateEvent] (const RoomAliasesEvent& ae) { // clang-format on if (ae.aliases().isEmpty()) { qCDebug(STATE).noquote() << ae.stateKey() << "no more has aliases for room" << objectName(); d->aliasServers.remove(ae.stateKey()); } else { d->aliasServers.insert(ae.stateKey()); qCDebug(STATE).nospace().noquote() << "New server with aliases for room " << objectName() << ": " << ae.stateKey(); } const auto previousAliases = oldStateEvent ? static_cast(oldStateEvent)->aliases() : QStringList(); connection()->updateRoomAliases(id(), ae.stateKey(), previousAliases, ae.aliases()); return OtherChange; // clang-format off } , [this] (const RoomCanonicalAliasEvent& evt) { setObjectName(evt.alias().isEmpty() ? d->id : evt.alias()); return CanonicalAliasChange; } , [] (const RoomTopicEvent&) { return TopicChange; } , [this] (const RoomAvatarEvent& evt) { if (d->avatar.updateUrl(evt.url())) emit avatarChanged(); return AvatarChange; } , [this,oldStateEvent] (const RoomMemberEvent& evt) { // clang-format on auto* u = user(evt.userId()); const auto* oldMemberEvent = static_cast(oldStateEvent); u->processEvent(evt, this, oldMemberEvent == nullptr); const auto prevMembership = oldMemberEvent ? oldMemberEvent->membership() : MembershipType::Leave; if (u == localUser() && evt.membership() == MembershipType::Invite && evt.isDirect()) connection()->addToDirectChats(this, user(evt.senderId())); switch (prevMembership) { case MembershipType::Invite: if (evt.membership() != prevMembership) { d->usersInvited.removeOne(u); Q_ASSERT(!d->usersInvited.contains(u)); } break; case MembershipType::Join: if (evt.membership() == MembershipType::Invite) qCWarning(STATE) << "Invalid membership change from " "Join to Invite:" << evt; if (evt.membership() != prevMembership) { disconnect(u, &User::nameAboutToChange, this, nullptr); disconnect(u, &User::nameChanged, this, nullptr); d->removeMemberFromMap(u->name(this), u); emit userRemoved(u); } break; default: if (evt.membership() == MembershipType::Invite || evt.membership() == MembershipType::Join) { d->membersLeft.removeOne(u); Q_ASSERT(!d->membersLeft.contains(u)); } } switch (evt.membership()) { case MembershipType::Join: if (prevMembership != MembershipType::Join) { d->insertMemberIntoMap(u); connect(u, &User::nameAboutToChange, this, [=](QString newName, QString, const Room* context) { if (context == this) emit memberAboutToRename(u, newName); }); connect(u, &User::nameChanged, this, [=](QString, QString oldName, const Room* context) { if (context == this) { d->renameMember(u, oldName); emit memberRenamed(u); } }); emit userAdded(u); } break; case MembershipType::Invite: if (!d->usersInvited.contains(u)) d->usersInvited.push_back(u); break; default: if (!d->membersLeft.contains(u)) d->membersLeft.append(u); } return MembersChange; // clang-format off } , [this] (const EncryptionEvent&) { emit encryption(); // It can only be done once, so emit it here. return OtherChange; } , [this] (const RoomTombstoneEvent& evt) { const auto successorId = evt.successorRoomId(); if (auto* successor = connection()->room(successorId)) emit upgraded(evt.serverMessage(), successor); else connectUntil(connection(), &Connection::loadedRoomState, this, [this,successorId,serverMsg=evt.serverMessage()] (Room* newRoom) { if (newRoom->id() != successorId) return false; emit upgraded(serverMsg, newRoom); return true; }); return OtherChange; } ); // clang-format on } Room::Changes Room::processEphemeralEvent(EventPtr&& event) { Changes changes = NoChange; QElapsedTimer et; et.start(); if (auto* evt = eventCast(event)) { d->usersTyping.clear(); for (const QString& userId : qAsConst(evt->users())) { auto u = user(userId); if (memberJoinState(u) == JoinState::Join) d->usersTyping.append(u); } if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):" << evt->users().size() << "users," << et; emit typingChanged(); } if (auto* evt = eventCast(event)) { int totalReceipts = 0; for (const auto& p : qAsConst(evt->eventsWithReceipts())) { totalReceipts += p.receipts.size(); { if (p.receipts.size() == 1) qCDebug(EPHEMERAL) << "Marking" << p.evtId << "as read for" << p.receipts[0].userId; else qCDebug(EPHEMERAL) << "Marking" << p.evtId << "as read for" << p.receipts.size() << "users"; } const auto newMarker = findInTimeline(p.evtId); if (newMarker != timelineEdge()) { for (const Receipt& r : p.receipts) { if (r.userId == connection()->userId()) continue; // FIXME, #185 auto u = user(r.userId); if (memberJoinState(u) == JoinState::Join) changes |= d->promoteReadMarker(u, newMarker); } } else { qCDebug(EPHEMERAL) << "Event" << p.evtId << "not found; saving read receipts anyway"; // If the event is not found (most likely, because it's too old // and hasn't been fetched from the server yet), but there is // a previous marker for a user, keep the previous marker. // Otherwise, blindly store the event id for this user. for (const Receipt& r : p.receipts) { if (r.userId == connection()->userId()) continue; // FIXME, #185 auto u = user(r.userId); if (memberJoinState(u) == JoinState::Join && readMarker(u) == timelineEdge()) changes |= d->setLastReadEvent(u, p.evtId); } } } if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** Room::processEphemeralEvent(receipts):" << evt->eventsWithReceipts().size() << "event(s) with" << totalReceipts << "receipt(s)," << et; } return changes; } Room::Changes Room::processAccountDataEvent(EventPtr&& event) { Changes changes = NoChange; if (auto* evt = eventCast(event)) { d->setTags(evt->tags()); changes |= Change::TagsChange; } if (auto* evt = eventCast(event)) { auto readEventId = evt->event_id(); qCDebug(STATE) << "Server-side read marker at" << readEventId; d->serverReadMarker = readEventId; const auto newMarker = findInTimeline(readEventId); changes |= newMarker != timelineEdge() ? d->markMessagesAsRead(newMarker) : d->setLastReadEvent(localUser(), readEventId); } // For all account data events auto& currentData = d->accountData[event->matrixType()]; // A polymorphic event-specific comparison might be a bit more // efficient; maaybe do it another day if (!currentData || currentData->contentJson() != event->contentJson()) { emit accountDataAboutToChange(event->matrixType()); currentData = move(event); qCDebug(STATE) << "Updated account data of type" << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); return Change::AccountDataChange; } return Change::NoChange; } template Room::Private::users_shortlist_t Room::Private::buildShortlist(const ContT& users) const { // To calculate room display name the spec requires to sort users // lexicographically by state_key (user id) and use disambiguated // display names of two topmost users excluding the current one to render // the name of the room. The below code selects 3 topmost users, // slightly extending the spec. users_shortlist_t shortlist {}; // Prefill with nullptrs std::partial_sort_copy( users.begin(), users.end(), shortlist.begin(), shortlist.end(), [this](const User* u1, const User* u2) { // localUser(), if it's in the list, is sorted // below all others return isLocalUser(u2) || (!isLocalUser(u1) && u1->id() < u2->id()); }); return shortlist; } Room::Private::users_shortlist_t Room::Private::buildShortlist(const QStringList& userIds) const { QList users; users.reserve(userIds.size()); for (const auto& h : userIds) users.push_back(q->user(h)); return buildShortlist(users); } QString Room::Private::calculateDisplayname() const { // CS spec, section 13.2.2.5 Calculating the display name for a room // Numbers below refer to respective parts in the spec. // 1. Name (from m.room.name) auto dispName = q->name(); if (!dispName.isEmpty()) { return dispName; } // 2. Canonical alias dispName = q->canonicalAlias(); if (!dispName.isEmpty()) return dispName; // 3. m.room.aliases - only local aliases, subject for further removal const auto aliases = q->localAliases(); if (!aliases.isEmpty()) return aliases.front(); // 4. m.heroes and m.room.member // From here on, we use a more general algorithm than the spec describes // in order to provide back-compatibility with pre-MSC688 servers. // Supplementary code: build the shortlist of users whose names // will be used to construct the room name. Takes into account MSC688's // "heroes" if available. const bool localUserIsIn = joinState == JoinState::Join; const bool emptyRoom = membersMap.isEmpty() || (membersMap.size() == 1 && isLocalUser(*membersMap.begin())); const bool nonEmptySummary = !summary.heroes.omitted() && !summary.heroes->empty(); auto shortlist = nonEmptySummary ? buildShortlist(summary.heroes.value()) : !emptyRoom ? buildShortlist(membersMap) : users_shortlist_t {}; // When the heroes list is there, we can rely on it. If the heroes list is // missing, the below code gathers invited, or, if there are no invitees, // left members. if (!shortlist.front() && localUserIsIn) shortlist = buildShortlist(usersInvited); if (!shortlist.front()) shortlist = buildShortlist(membersLeft); QStringList names; for (auto u : shortlist) { if (u == nullptr || isLocalUser(u)) break; // Only disambiguate if the room is not empty names.push_back(u->displayname(emptyRoom ? nullptr : q)); } const auto usersCountExceptLocal = !emptyRoom ? q->joinedCount() - int(joinState == JoinState::Join) : !usersInvited.empty() ? usersInvited.count() : membersLeft.size() - int(joinState == JoinState::Leave); if (usersCountExceptLocal > int(shortlist.size())) names << tr( "%Ln other(s)", "Used to make a room name from user names: A, B and _N others_", usersCountExceptLocal - int(shortlist.size())); const auto namesList = QLocale().createSeparatedList(names); // Room members if (!emptyRoom) return namesList; // (Spec extension) Invited users if (!usersInvited.empty()) return tr("Empty room (invited: %1)").arg(namesList); // Users that previously left the room if (!membersLeft.isEmpty()) return tr("Empty room (was: %1)").arg(namesList); // Fail miserably return tr("Empty room (%1)").arg(id); } void Room::Private::updateDisplayname() { auto swappedName = calculateDisplayname(); if (swappedName != displayname) { emit q->displaynameAboutToChange(q); swap(displayname, swappedName); qCDebug(MAIN) << q->objectName() << "has changed display name from" << swappedName << "to" << displayname; emit q->displaynameChanged(q, swappedName); } } QJsonObject Room::Private::toJson() const { QElapsedTimer et; et.start(); QJsonObject result; addParam(result, QStringLiteral("summary"), summary); { QJsonArray stateEvents; for (const auto* evt : currentState) { Q_ASSERT(evt->isStateEvent()); if ((evt->isRedacted() && !is(*evt)) || evt->contentJson().isEmpty()) continue; auto json = evt->fullJson(); auto unsignedJson = evt->unsignedJson(); unsignedJson.remove(QStringLiteral("prev_content")); json[UnsignedKeyL] = unsignedJson; stateEvents.append(json); } const auto stateObjName = joinState == JoinState::Invite ? QStringLiteral("invite_state") : QStringLiteral("state"); result.insert(stateObjName, QJsonObject { { QStringLiteral("events"), stateEvents } }); } if (!accountData.empty()) { QJsonArray accountDataEvents; for (const auto& e : accountData) { if (!e.second->contentJson().isEmpty()) accountDataEvents.append(e.second->fullJson()); } result.insert(QStringLiteral("account_data"), QJsonObject { { QStringLiteral("events"), accountDataEvents } }); } QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey, unreadMessages } }; if (highlightCount > 0) unreadNotifObj.insert(QStringLiteral("highlight_count"), highlightCount); if (notificationCount > 0) unreadNotifObj.insert(QStringLiteral("notification_count"), notificationCount); result.insert(QStringLiteral("unread_notifications"), unreadNotifObj); if (et.elapsed() > 30) qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et; return result; } QJsonObject Room::toJson() const { return d->toJson(); } MemberSorter Room::memberSorter() const { return MemberSorter(this); } bool MemberSorter::operator()(User* u1, User* u2) const { return operator()(u1, room->roomMembername(u2)); } bool MemberSorter::operator()(User* u1, const QString& u2name) const { auto n1 = room->roomMembername(u1); if (n1.startsWith('@')) n1.remove(0, 1); auto n2 = u2name.midRef(u2name.startsWith('@') ? 1 : 0); return n1.localeAwareCompare(n2) < 0; } spectral/include/libQuotient/lib/util.cpp0000644000175000000620000001411613566674122020540 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "util.h" #include #include #include #include #include #include #include static const auto RegExpOptions = QRegularExpression::CaseInsensitiveOption | QRegularExpression::OptimizeOnFirstUsageOption | QRegularExpression::UseUnicodePropertiesOption; // Converts all that looks like a URL into HTML links void Quotient::linkifyUrls(QString& htmlEscapedText) { // Note: outer parentheses are a part of C++ raw string delimiters, not of // the regex (see http://en.cppreference.com/w/cpp/language/string_literal). // Note2: the next-outer parentheses are \N in the replacement. // generic url: // regexp is originally taken from Konsole (https://github.com/KDE/konsole) // protocolname:// or www. followed by anything other than whitespaces, // <, >, ' or ", and ends before whitespaces, <, >, ', ", ], !, ), :, // comma or dot static const QRegularExpression FullUrlRegExp( QStringLiteral( R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet|matrix):(//)?)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"), RegExpOptions); // email address: // [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] static const QRegularExpression EmailAddressRegExp( QStringLiteral(R"(\b(mailto:)?((\w|\.|-)+@(\w|\.|-)+\.\w+\b))"), RegExpOptions); // An interim liberal implementation of // https://matrix.org/docs/spec/appendices.html#identifier-grammar static const QRegularExpression MxIdRegExp( QStringLiteral( R"((^|[^<>/])([!#@][-a-z0-9_=/.]{1,252}:(?:\w|\.|-)+\.\w+(?::\d{1,5})?))"), RegExpOptions); // NOTE: htmlEscapedText is already HTML-escaped! No literal <,>,&," htmlEscapedText.replace(EmailAddressRegExp, QStringLiteral(R"(\1\2)")); htmlEscapedText.replace(FullUrlRegExp, QStringLiteral(R"(\1)")); htmlEscapedText.replace( MxIdRegExp, QStringLiteral(R"(\1\2)")); } QString Quotient::sanitized(const QString& plainText) { auto text = plainText; text.remove(QChar(0x202e)); // RLO text.remove(QChar(0x202d)); // LRO text.remove(QChar(0xfffc)); // Object replacement character return text; } QString Quotient::prettyPrint(const QString& plainText) { auto pt = plainText.toHtmlEscaped(); linkifyUrls(pt); pt.replace('\n', QStringLiteral("
    ")); return QStringLiteral("") + pt + QStringLiteral(""); } QString Quotient::cacheLocation(const QString& dirName) { const QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) % '/' % dirName % '/'; QDir dir; if (!dir.exists(cachePath)) dir.mkpath(cachePath); return cachePath; } qreal Quotient::stringToHueF(const QString& s) { Q_ASSERT(!s.isEmpty()); QByteArray hash = QCryptographicHash::hash(s.toUtf8(), QCryptographicHash::Sha1); QDataStream dataStream(qToLittleEndian(hash).left(2)); dataStream.setByteOrder(QDataStream::LittleEndian); quint16 hashValue; dataStream >> hashValue; const auto hueF = qreal(hashValue) / std::numeric_limits::max(); Q_ASSERT((0 <= hueF) && (hueF <= 1)); return hueF; } static const auto ServerPartRegEx = QStringLiteral( "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address "(?::(\\d{1,5}))?" // Optional port ); QString Quotient::serverPart(const QString& mxId) { static QString re = "^[@!#$+].+?:(" // Localpart and colon % ServerPartRegEx % ")$"; static QRegularExpression parser( re, QRegularExpression::UseUnicodePropertiesOption); // Because Asian // digits return parser.match(mxId).captured(1); } // Tests for function_traits<> #ifdef Q_CC_CLANG # pragma clang diagnostic push # pragma ide diagnostic ignored "OCSimplifyInspection" #endif using namespace Quotient; int f(); static_assert(std::is_same, int>::value, "Test fn_return_t<>"); void f1(int, QString); static_assert(std::is_same, QString>::value, "Test fn_arg_t<>"); struct Fo { int operator()(); }; static_assert(std::is_same, int>::value, "Test return type of function object"); struct Fo1 { void operator()(int); }; static_assert(std::is_same, int>(), "Test fn_arg_t defaulting to first argument"); #if (!defined(_MSC_VER) || _MSC_VER >= 1910) static auto l = [] { return 1; }; static_assert(std::is_same, int>::value, "Test fn_return_t<> with lambda"); #endif template QString ft(T&&) { return {}; } static_assert(std::is_same)>, QString&&>(), "Test function templates"); #ifdef Q_CC_CLANG # pragma clang diagnostic pop #endif spectral/include/libQuotient/lib/encryptionmanager.h0000644000175000000620000000152213566674122022752 0ustar dilingerstaff#pragma once #include #include #include namespace QtOlm { class Account; } namespace Quotient { class Connection; class EncryptionManager : public QObject { Q_OBJECT public: // TODO: store constats separately? // TODO: 0.5 oneTimeKeyThreshold instead of 0.1? explicit EncryptionManager( const QByteArray& encryptionAccountPickle = QByteArray(), float signedKeysProportion = 1, float oneTimeKeyThreshold = float(0.1), QObject* parent = nullptr); ~EncryptionManager(); void uploadIdentityKeys(Connection* connection); void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false); QByteArray olmAccountPickle(); QtOlm::Account* account() const; private: class Private; std::unique_ptr d; }; } // namespace Quotient spectral/include/libQuotient/lib/eventitem.cpp0000644000175000000620000000316213566674122021562 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "eventitem.h" #include "events/roomavatarevent.h" #include "events/roommessageevent.h" using namespace Quotient; void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) { // TODO: eventually we might introduce hasFileContent to RoomEvent, // and unify the code below. if (auto* rme = getAs()) { Q_ASSERT(rme->hasFileContent()); rme->editContent([remoteUrl](EventContent::TypedBase& ec) { ec.fileInfo()->url = remoteUrl; }); } if (auto* rae = getAs()) { Q_ASSERT(rae->content().fileInfo()); rae->editContent( [remoteUrl](EventContent::FileInfo& fi) { fi.url = remoteUrl; }); } setStatus(EventStatus::FileUploaded); } spectral/include/libQuotient/lib/util.h0000644000175000000620000002274513566674122020214 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2016 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include #include #include #include #include // Along the lines of Q_DISABLE_COPY - the upstream version comes in Qt 5.13 #define DISABLE_MOVE(_ClassName) \ _ClassName(_ClassName&&) Q_DECL_EQ_DELETE; \ _ClassName& operator=(_ClassName&&) Q_DECL_EQ_DELETE; namespace Quotient { /// An equivalent of std::hash for QTypes to enable std::unordered_map template struct HashQ { size_t operator()(const T& s) const Q_DECL_NOEXCEPT { return qHash(s, uint(qGlobalQHashSeed())); } }; /// A wrapper around std::unordered_map compatible with types that have qHash template using UnorderedMap = std::unordered_map>; struct NoneTag {}; constexpr NoneTag none {}; /** A crude substitute for `optional` while we're not C++17 * * Only works with default-constructible types. */ template class Omittable { static_assert(!std::is_reference::value, "You cannot make an Omittable<> with a reference type"); public: using value_type = std::decay_t; explicit Omittable() : Omittable(none) {} Omittable(NoneTag) : _value(value_type()), _omitted(true) {} Omittable(const value_type& val) : _value(val) {} Omittable(value_type&& val) : _value(std::move(val)) {} Omittable& operator=(const value_type& val) { _value = val; _omitted = false; return *this; } Omittable& operator=(value_type&& val) { // For some reason GCC complains about -Wmaybe-uninitialized // in the context of using Omittable with converters.h; // though the logic looks very much benign (GCC bug???) _value = std::move(val); _omitted = false; return *this; } bool operator==(const value_type& rhs) const { return !omitted() && value() == rhs; } friend bool operator==(const value_type& lhs, const Omittable& rhs) { return rhs == lhs; } bool operator!=(const value_type& rhs) const { return !operator==(rhs); } friend bool operator!=(const value_type& lhs, const Omittable& rhs) { return !(rhs == lhs); } bool omitted() const { return _omitted; } const value_type& value() const { Q_ASSERT(!_omitted); return _value; } value_type& editValue() { _omitted = false; return _value; } /// Merge the value from another Omittable /// \return true if \p other is not omitted and the value of /// the current Omittable was different (or omitted); /// in other words, if the current Omittable has changed; /// false otherwise template auto merge(const Omittable& other) -> std::enable_if_t::value, bool> { if (other.omitted() || (!_omitted && _value == other.value())) return false; _omitted = false; _value = other.value(); return true; } value_type&& release() { _omitted = true; return std::move(_value); } const value_type* operator->() const& { return &value(); } value_type* operator->() & { return &editValue(); } const value_type& operator*() const& { return value(); } value_type& operator*() & { return editValue(); } private: T _value; bool _omitted = false; }; namespace _impl { template struct fn_traits; } /// Determine traits of an arbitrary function/lambda/functor /*! * Doesn't work with generic lambdas and function objects that have * operator() overloaded. * \sa * https://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda#7943765 */ template struct function_traits : public _impl::fn_traits> {}; // Specialisation for a function template struct function_traits { using return_type = ReturnT; using arg_types = std::tuple; // Doesn't (and there's no plan to make it) work for "classic" // member functions (i.e. outside of functors). // See also the comment for wrap_in_function() below using function_type = std::function; }; namespace _impl { // Specialisation for function objects with (non-overloaded) operator() // (this includes non-generic lambdas) template struct fn_traits : public fn_traits {}; // Specialisation for a member function template struct fn_traits : function_traits {}; // Specialisation for a const member function template struct fn_traits : function_traits {}; } // namespace _impl template using fn_return_t = typename function_traits::return_type; template using fn_arg_t = std::tuple_element_t::arg_types>; // TODO: get rid of it as soon as Apple Clang gets proper deduction guides // for std::function<> template inline auto wrap_in_function(FnT&& f) { return typename function_traits::function_type(std::forward(f)); } inline auto operator"" _ls(const char* s, std::size_t size) { return QLatin1String(s, int(size)); } /** An abstraction over a pair of iterators * This is a very basic range type over a container with iterators that * are at least ForwardIterators. Inspired by Ranges TS. */ template class Range { // Looking forward for Ranges TS to produce something (in C++23?..) using iterator = typename ArrayT::iterator; using const_iterator = typename ArrayT::const_iterator; using size_type = typename ArrayT::size_type; public: Range(ArrayT& arr) : from(std::begin(arr)), to(std::end(arr)) {} Range(iterator from, iterator to) : from(from), to(to) {} size_type size() const { Q_ASSERT(std::distance(from, to) >= 0); return size_type(std::distance(from, to)); } bool empty() const { return from == to; } const_iterator begin() const { return from; } const_iterator end() const { return to; } iterator begin() { return from; } iterator end() { return to; } private: iterator from; iterator to; }; /** A replica of std::find_first_of that returns a pair of iterators * * Convenient for cases when you need to know which particular "first of" * [sFirst, sLast) has been found in [first, last). */ template inline std::pair findFirstOf(InputIt first, InputIt last, ForwardIt sFirst, ForwardIt sLast, Pred pred) { for (; first != last; ++first) for (auto it = sFirst; it != sLast; ++it) if (pred(*first, *it)) return std::make_pair(first, it); return std::make_pair(last, sLast); } /** Convert what looks like a URL or a Matrix ID to an HTML hyperlink */ void linkifyUrls(QString& htmlEscapedText); /** Sanitize the text before showing in HTML * * This does toHtmlEscaped() and removes Unicode BiDi marks. */ QString sanitized(const QString& plainText); /** Pretty-print plain text into HTML * * This includes HTML escaping of <,>,",& and calling linkifyUrls() */ QString prettyPrint(const QString& plainText); /** Return a path to cache directory after making sure that it exists * * The returned path has a trailing slash, clients don't need to append it. * \param dir path to cache directory relative to the standard cache path */ QString cacheLocation(const QString& dirName); /** Hue color component of based of the hash of the string. * * The implementation is based on XEP-0392: * https://xmpp.org/extensions/xep-0392.html * Naming and range are the same as QColor's hueF method: * https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision */ qreal stringToHueF(const QString& s); /** Extract the serverpart from MXID */ QString serverPart(const QString& mxId); } // namespace Quotient /// \deprecated Use namespace Quotient instead namespace QMatrixClient = Quotient; spectral/include/libQuotient/lib/events/0002755000175000000620000000000013566674122020362 5ustar dilingerstaffspectral/include/libQuotient/lib/events/roomcreateevent.cpp0000644000175000000620000000273213566674122024272 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2019 QMatrixClient project * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "roomcreateevent.h" using namespace Quotient; bool RoomCreateEvent::isFederated() const { return fromJson(contentJson()["m.federate"_ls]); } QString RoomCreateEvent::version() const { return fromJson(contentJson()["room_version"_ls]); } RoomCreateEvent::Predecessor RoomCreateEvent::predecessor() const { const auto predJson = contentJson()["predecessor"_ls].toObject(); return { fromJson(predJson["room_id"_ls]), fromJson(predJson["event_id"_ls]) }; } bool RoomCreateEvent::isUpgrade() const { return contentJson().contains("predecessor"_ls); } spectral/include/libQuotient/lib/events/callanswerevent.h0000644000175000000620000000307613566674122023734 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Marius Gripsgard * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "roomevent.h" namespace Quotient { class CallAnswerEvent : public CallEventBase { public: DEFINE_EVENT_TYPEID("m.call.answer", CallAnswerEvent) explicit CallAnswerEvent(const QJsonObject& obj); explicit CallAnswerEvent(const QString& callId, const int lifetime, const QString& sdp); explicit CallAnswerEvent(const QString& callId, const QString& sdp); int lifetime() const { return content("lifetime"_ls); } // FIXME: Omittable<>? QString sdp() const { return contentJson()["answer"_ls].toObject().value("sdp"_ls).toString(); } }; REGISTER_EVENT_TYPE(CallAnswerEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/roomtombstoneevent.h0000644000175000000620000000254313566674122024506 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2019 QMatrixClient project * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "stateevent.h" namespace Quotient { class RoomTombstoneEvent : public StateEventBase { public: DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent) explicit RoomTombstoneEvent() : StateEventBase(typeId(), matrixTypeId()) {} explicit RoomTombstoneEvent(const QJsonObject& obj) : StateEventBase(typeId(), obj) {} QString serverMessage() const; QString successorRoomId() const; }; REGISTER_EVENT_TYPE(RoomTombstoneEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/callcandidatesevent.cpp0000644000175000000620000000264213566674122025065 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Marius Gripsgard * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "callcandidatesevent.h" /* m.call.candidates { "age": 242352, "content": { "call_id": "12345", "candidates": [ { "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0", "sdpMLineIndex": 0, "sdpMid": "audio" } ], "version": 0 }, "event_id": "$WLGTSEFSEF:localhost", "origin_server_ts": 1431961217939, "room_id": "!Cuyf34gef24t:localhost", "sender": "@example:localhost", "type": "m.call.candidates" } */ spectral/include/libQuotient/lib/events/reactionevent.cpp0000644000175000000620000000325113566674122023733 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2019 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "reactionevent.h" using namespace Quotient; void JsonObjectConverter::dumpTo( QJsonObject& jo, const EventRelation& pod) { if (pod.type.isEmpty()) { qCWarning(MAIN) << "Empty relation type; won't dump to JSON"; return; } jo.insert(QStringLiteral("rel_type"), pod.type); jo.insert(EventIdKey, pod.eventId); if (pod.type == EventRelation::Annotation()) jo.insert(QStringLiteral("key"), pod.key); } void JsonObjectConverter::fillFrom( const QJsonObject& jo, EventRelation& pod) { // The experimental logic for generic relationships (MSC1849) fromJson(jo["rel_type"_ls], pod.type); fromJson(jo[EventIdKeyL], pod.eventId); if (pod.type == EventRelation::Annotation()) fromJson(jo["key"_ls], pod.key); } spectral/include/libQuotient/lib/events/event.h0000644000175000000620000002761213566674122021662 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "converters.h" #include "logging.h" #ifdef ENABLE_EVENTTYPE_ALIAS # define USE_EVENTTYPE_ALIAS 1 #endif namespace Quotient { // === event_ptr_tt<> and type casting facilities === template using event_ptr_tt = std::unique_ptr; /// Unwrap a plain pointer from a smart pointer template inline EventT* rawPtr(const event_ptr_tt& ptr) { return ptr.get(); } /// Unwrap a plain pointer and downcast it to the specified type template inline TargetEventT* weakPtrCast(const event_ptr_tt& ptr) { return static_cast(rawPtr(ptr)); } /// Re-wrap a smart pointer to base into a smart pointer to derived template [[deprecated("Consider using eventCast() or visit() instead")]] inline event_ptr_tt ptrCast(event_ptr_tt&& ptr) { return std::unique_ptr(static_cast(ptr.release())); } // === Standard Matrix key names and basicEventJson() === static const auto TypeKey = QStringLiteral("type"); static const auto BodyKey = QStringLiteral("body"); static const auto ContentKey = QStringLiteral("content"); static const auto EventIdKey = QStringLiteral("event_id"); static const auto UnsignedKey = QStringLiteral("unsigned"); static const auto StateKeyKey = QStringLiteral("state_key"); static const auto TypeKeyL = "type"_ls; static const auto BodyKeyL = "body"_ls; static const auto ContentKeyL = "content"_ls; static const auto EventIdKeyL = "event_id"_ls; static const auto UnsignedKeyL = "unsigned"_ls; static const auto RedactedCauseKeyL = "redacted_because"_ls; static const auto PrevContentKeyL = "prev_content"_ls; static const auto StateKeyKeyL = "state_key"_ls; /// Make a minimal correct Matrix event JSON template inline QJsonObject basicEventJson(StrT matrixType, const QJsonObject& content) { return { { TypeKey, std::forward(matrixType) }, { ContentKey, content } }; } // === Event types and event types registry === using event_type_t = size_t; using event_mtype_t = const char*; class EventTypeRegistry { public: ~EventTypeRegistry() = default; static event_type_t initializeTypeId(event_mtype_t matrixTypeId); template static inline event_type_t initializeTypeId() { return initializeTypeId(EventT::matrixTypeId()); } static QString getMatrixType(event_type_t typeId); private: EventTypeRegistry() = default; Q_DISABLE_COPY(EventTypeRegistry) DISABLE_MOVE(EventTypeRegistry) static EventTypeRegistry& get() { static EventTypeRegistry etr; return etr; } std::vector eventTypes; }; template <> inline event_type_t EventTypeRegistry::initializeTypeId() { return initializeTypeId(""); } template struct EventTypeTraits { static event_type_t id() { static const auto id = EventTypeRegistry::initializeTypeId(); return id; } }; template inline event_type_t typeId() { return EventTypeTraits>::id(); } inline event_type_t unknownEventTypeId() { return typeId(); } // === EventFactory === /** Create an event of arbitrary type from its arguments */ template inline event_ptr_tt makeEvent(ArgTs&&... args) { return std::make_unique(std::forward(args)...); } template class EventFactory { public: template static auto addMethod(FnT&& method) { factories().emplace_back(std::forward(method)); return 0; } /** Chain two type factories * Adds the factory class of EventT2 (EventT2::factory_t) to * the list in factory class of EventT1 (EventT1::factory_t) so * that when EventT1::factory_t::make() is invoked, types of * EventT2 factory are looked through as well. This is used * to include RoomEvent types into the more general Event factory, * and state event types into the RoomEvent factory. */ template static auto chainFactory() { return addMethod(&EventT::factory_t::make); } static event_ptr_tt make(const QJsonObject& json, const QString& matrixType) { for (const auto& f : factories()) if (auto e = f(json, matrixType)) return e; return nullptr; } private: static auto& factories() { using inner_factory_tt = std::function( const QJsonObject&, const QString&)>; static std::vector _factories {}; return _factories; } }; /** Add a type to its default factory * Adds a standard factory method (via makeEvent<>) for a given * type to EventT::factory_t factory class so that it can be * created dynamically from loadEvent<>(). * * \tparam EventT the type to enable dynamic creation of * \return the registered type id * \sa loadEvent, Event::type */ template inline auto setupFactory() { qDebug(EVENTS) << "Adding factory method for" << EventT::matrixTypeId(); return EventT::factory_t::addMethod([](const QJsonObject& json, const QString& jsonMatrixType) { return EventT::matrixTypeId() == jsonMatrixType ? makeEvent(json) : nullptr; }); } template inline auto registerEventType() { // Initialise exactly once, even if this function is called twice for // the same type (for whatever reason - you never know the ways of // static initialisation is done). static const auto _ = setupFactory(); return _; // Only to facilitate usage in static initialisation } // === Event === class Event { Q_GADGET Q_PROPERTY(Type type READ type CONSTANT) Q_PROPERTY(QJsonObject contentJson READ contentJson CONSTANT) public: using Type = event_type_t; using factory_t = EventFactory; explicit Event(Type type, const QJsonObject& json); explicit Event(Type type, event_mtype_t matrixType, const QJsonObject& contentJson = {}); Q_DISABLE_COPY(Event) Event(Event&&) = default; Event& operator=(Event&&) = delete; virtual ~Event(); Type type() const { return _type; } QString matrixType() const; QByteArray originalJson() const; QJsonObject originalJsonObject() const { return fullJson(); } const QJsonObject& fullJson() const { return _json; } // According to the CS API spec, every event also has // a "content" object; but since its structure is different for // different types, we're implementing it per-event type. const QJsonObject contentJson() const; const QJsonObject unsignedJson() const; template T content(const QString& key) const { return fromJson(contentJson()[key]); } template T content(QLatin1String key) const { return fromJson(contentJson()[key]); } friend QDebug operator<<(QDebug dbg, const Event& e) { QDebugStateSaver _dss { dbg }; dbg.noquote().nospace() << e.matrixType() << '(' << e.type() << "): "; e.dumpTo(dbg); return dbg; } virtual bool isStateEvent() const { return false; } virtual bool isCallEvent() const { return false; } virtual void dumpTo(QDebug dbg) const; protected: QJsonObject& editJson() { return _json; } private: Type _type; QJsonObject _json; }; using EventPtr = event_ptr_tt; template using EventsArray = std::vector>; using Events = EventsArray; // === Macros used with event class definitions === // This macro should be used in a public section of an event class to // provide matrixTypeId() and typeId(). #define DEFINE_EVENT_TYPEID(_Id, _Type) \ static constexpr event_mtype_t matrixTypeId() { return _Id; } \ static auto typeId() { return Quotient::typeId<_Type>(); } \ // End of macro // This macro should be put after an event class definition (in .h or .cpp) // to enable its deserialisation from a /sync and other // polymorphic event arrays #define REGISTER_EVENT_TYPE(_Type) \ namespace { \ [[maybe_unused]] static const auto _factoryAdded##_Type = \ registerEventType<_Type>(); \ } \ // End of macro // === is<>(), eventCast<>() and visit<>() === template inline bool is(const Event& e) { return e.type() == typeId(); } inline bool isUnknown(const Event& e) { return e.type() == unknownEventTypeId(); } template inline auto eventCast(const BasePtrT& eptr) -> decltype(static_cast(&*eptr)) { Q_ASSERT(eptr); return is>(*eptr) ? static_cast(&*eptr) : nullptr; } // A single generic catch-all visitor template inline auto visit(const BaseEventT& event, FnT&& visitor) -> decltype(visitor(event)) { return visitor(event); } namespace _impl { template constexpr auto needs_downcast() { return !std::is_convertible_v>; } } // A single type-specific void visitor template inline std::enable_if_t<_impl::needs_downcast() && std::is_void_v>> visit(const BaseEventT& event, FnT&& visitor) { using event_type = fn_arg_t; if (is>(event)) visitor(static_cast(event)); } // A single type-specific non-void visitor with an optional default value // non-voidness is guarded by defaultValue type template inline std::enable_if_t<_impl::needs_downcast(), fn_return_t> visit(const BaseEventT& event, FnT&& visitor, fn_return_t&& defaultValue = {}) { using event_type = fn_arg_t; if (is>(event)) return visitor(static_cast(event)); return std::forward>(defaultValue); } // A chain of 2 or more visitors template inline fn_return_t visit(const BaseEventT& event, FnT1&& visitor1, FnT2&& visitor2, FnTs&&... visitors) { using event_type1 = fn_arg_t; if (is>(event)) return visitor1(static_cast(event)); return visit(event, std::forward(visitor2), std::forward(visitors)...); } } // namespace Quotient Q_DECLARE_METATYPE(Quotient::Event*) Q_DECLARE_METATYPE(const Quotient::Event*) spectral/include/libQuotient/lib/events/roomavatarevent.h0000644000175000000620000000301513566674122023745 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "eventcontent.h" #include "stateevent.h" namespace Quotient { class RoomAvatarEvent : public StateEvent { // It's a bit of an overkill to use a full-fledged ImageContent // because in reality m.room.avatar usually only has a single URL, // without a thumbnail. But The Spec says there be thumbnails, and // we follow The Spec. public: DEFINE_EVENT_TYPEID("m.room.avatar", RoomAvatarEvent) explicit RoomAvatarEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} QUrl url() const { return content().url; } }; REGISTER_EVENT_TYPE(RoomAvatarEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/roomevent.cpp0000644000175000000620000001022613566674122023103 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "roomevent.h" #include "converters.h" #include "logging.h" #include "redactionevent.h" using namespace Quotient; [[maybe_unused]] static auto roomEventTypeInitialised = Event::factory_t::chainFactory(); RoomEvent::RoomEvent(Type type, event_mtype_t matrixType, const QJsonObject& contentJson) : Event(type, matrixType, contentJson) {} RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json) { const auto unsignedData = json[UnsignedKeyL].toObject(); const auto redaction = unsignedData[RedactedCauseKeyL]; if (redaction.isObject()) { _redactedBecause = makeEvent(redaction.toObject()); return; } } RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job QString RoomEvent::id() const { return fullJson()[EventIdKeyL].toString(); } QDateTime RoomEvent::timestamp() const { return Quotient::fromJson(fullJson()["origin_server_ts"_ls]); } QString RoomEvent::roomId() const { return fullJson()["room_id"_ls].toString(); } QString RoomEvent::senderId() const { return fullJson()["sender"_ls].toString(); } bool RoomEvent::isReplaced() const { return unsignedJson()["m.relations"_ls].toObject().contains("m.replace"); } QString RoomEvent::replacedBy() const { // clang-format off return unsignedJson()["m.relations"_ls].toObject() .value("m.replace").toObject() .value(EventIdKeyL).toString(); // clang-format on } QString RoomEvent::redactionReason() const { return isRedacted() ? _redactedBecause->reason() : QString {}; } QString RoomEvent::transactionId() const { return unsignedJson()["transaction_id"_ls].toString(); } QString RoomEvent::stateKey() const { return fullJson()[StateKeyKeyL].toString(); } void RoomEvent::setRoomId(const QString& roomId) { editJson().insert(QStringLiteral("room_id"), roomId); } void RoomEvent::setSender(const QString& senderId) { editJson().insert(QStringLiteral("sender"), senderId); } void RoomEvent::setTransactionId(const QString& txnId) { auto unsignedData = fullJson()[UnsignedKeyL].toObject(); unsignedData.insert(QStringLiteral("transaction_id"), txnId); editJson().insert(UnsignedKey, unsignedData); Q_ASSERT(transactionId() == txnId); } void RoomEvent::addId(const QString& newId) { Q_ASSERT(id().isEmpty()); Q_ASSERT(!newId.isEmpty()); editJson().insert(EventIdKey, newId); qCDebug(EVENTS) << "Event txnId -> id:" << transactionId() << "->" << id(); Q_ASSERT(id() == newId); } QJsonObject makeCallContentJson(const QString& callId, int version, QJsonObject content) { content.insert(QStringLiteral("call_id"), callId); content.insert(QStringLiteral("version"), version); return content; } CallEventBase::CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, int version, const QJsonObject& contentJson) : RoomEvent(type, matrixType, makeCallContentJson(callId, version, contentJson)) {} CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json) : RoomEvent(type, json) { if (callId().isEmpty()) qCWarning(EVENTS) << id() << "is a call event with an empty call id"; } spectral/include/libQuotient/lib/events/stateevent.h0000644000175000000620000001062413566674122022716 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "roomevent.h" namespace Quotient { /// Make a minimal correct Matrix state event JSON template inline QJsonObject basicStateEventJson(StrT matrixType, const QJsonObject& content, const QString& stateKey = {}) { return { { TypeKey, std::forward(matrixType) }, { StateKeyKey, stateKey }, { ContentKey, content } }; } class StateEventBase : public RoomEvent { public: using factory_t = EventFactory; StateEventBase(Type type, const QJsonObject& json) : RoomEvent(type, json) {} StateEventBase(Type type, event_mtype_t matrixType, const QString& stateKey = {}, const QJsonObject& contentJson = {}); ~StateEventBase() override = default; bool isStateEvent() const override { return true; } QString replacedState() const; void dumpTo(QDebug dbg) const override; virtual bool repeatsState() const; }; using StateEventPtr = event_ptr_tt; using StateEvents = EventsArray; template <> inline bool is(const Event& e) { return e.isStateEvent(); } /** * A combination of event type and state key uniquely identifies a piece * of state in Matrix. * \sa * https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events */ using StateEventKey = QPair; template struct Prev { template explicit Prev(const QJsonObject& unsignedJson, ContentParamTs&&... contentParams) : senderId(unsignedJson.value("prev_sender"_ls).toString()) , content(unsignedJson.value(PrevContentKeyL).toObject(), std::forward(contentParams)...) {} QString senderId; ContentT content; }; template class StateEvent : public StateEventBase { public: using content_type = ContentT; template explicit StateEvent(Type type, const QJsonObject& fullJson, ContentParamTs&&... contentParams) : StateEventBase(type, fullJson) , _content(contentJson(), std::forward(contentParams)...) { const auto& unsignedData = unsignedJson(); if (unsignedData.contains(PrevContentKeyL)) _prev = std::make_unique>( unsignedData, std::forward(contentParams)...); } template explicit StateEvent(Type type, event_mtype_t matrixType, const QString& stateKey, ContentParamTs&&... contentParams) : StateEventBase(type, matrixType, stateKey) , _content(std::forward(contentParams)...) { editJson().insert(ContentKey, _content.toJson()); } const ContentT& content() const { return _content; } template void editContent(VisitorT&& visitor) { visitor(_content); editJson()[ContentKeyL] = _content.toJson(); } [[deprecated("Use prevContent instead")]] const ContentT* prev_content() const { return prevContent(); } const ContentT* prevContent() const { return _prev ? &_prev->content : nullptr; } QString prevSenderId() const { return _prev ? _prev->senderId : QString(); } private: ContentT _content; std::unique_ptr> _prev; }; } // namespace Quotient spectral/include/libQuotient/lib/events/redactionevent.cpp0000644000175000000620000000003413566674122024073 0ustar dilingerstaff#include "redactionevent.h" spectral/include/libQuotient/lib/events/encryptionevent.h0000644000175000000620000000472113566674122023771 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "eventcontent.h" #include "stateevent.h" namespace Quotient { class EncryptionEventContent : public EventContent::Base { public: enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined }; explicit EncryptionEventContent(EncryptionType et = Undefined) : encryption(et) {} explicit EncryptionEventContent(const QJsonObject& json); EncryptionType encryption; QString algorithm; int rotationPeriodMs; int rotationPeriodMsgs; protected: void fillJson(QJsonObject* o) const override; }; using EncryptionType = EncryptionEventContent::EncryptionType; class EncryptionEvent : public StateEvent { Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.encryption", EncryptionEvent) using EncryptionType = EncryptionEventContent::EncryptionType; explicit EncryptionEvent(const QJsonObject& obj = {}) // TODO: apropriate // default value : StateEvent(typeId(), obj) {} template EncryptionEvent(ArgTs&&... contentArgs) : StateEvent(typeId(), matrixTypeId(), QString(), std::forward(contentArgs)...) {} EncryptionType encryption() const { return content().encryption; } QString algorithm() const { return content().algorithm; } int rotationPeriodMs() const { return content().rotationPeriodMs; } int rotationPeriodMsgs() const { return content().rotationPeriodMsgs; } private: Q_ENUM(EncryptionType) }; REGISTER_EVENT_TYPE(EncryptionEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/accountdataevents.h0000644000175000000620000000761213566674122024252 0ustar dilingerstaff#include /****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "converters.h" #include "event.h" #include "eventcontent.h" namespace Quotient { constexpr const char* FavouriteTag = "m.favourite"; constexpr const char* LowPriorityTag = "m.lowpriority"; constexpr const char* ServerNoticeTag = "m.server_notice"; struct TagRecord { using order_type = Omittable; order_type order; TagRecord(order_type order = none) : order(order) {} bool operator<(const TagRecord& other) const { // Per The Spec, rooms with no order should be after those with order return !order.omitted() && (other.order.omitted() || order.value() < other.order.value()); } }; template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, TagRecord& rec) { // Parse a float both from JSON double and JSON string because // the library previously used to use strings to store order. const auto orderJv = jo.value("order"_ls); if (orderJv.isDouble()) rec.order = fromJson(orderJv); if (orderJv.isString()) { bool ok; rec.order = orderJv.toString().toFloat(&ok); if (!ok) rec.order = none; } } static void dumpTo(QJsonObject& jo, const TagRecord& rec) { addParam(jo, QStringLiteral("order"), rec.order); } }; using TagsMap = QHash; #define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ class _Name : public Event { \ public: \ using content_type = _ContentType; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ explicit _Name(QJsonObject obj) : Event(typeId(), std::move(obj)) {} \ explicit _Name(_ContentType content) \ : Event(typeId(), matrixTypeId(), \ QJsonObject { { QStringLiteral(#_ContentKey), \ toJson(std::move(content)) } }) \ {} \ auto _ContentKey() const \ { \ return content(#_ContentKey##_ls); \ } \ }; \ REGISTER_EVENT_TYPE(_Name) \ // End of macro DEFINE_SIMPLE_EVENT(TagEvent, "m.tag", TagsMap, tags) DEFINE_SIMPLE_EVENT(ReadMarkerEvent, "m.fully_read", QString, event_id) DEFINE_SIMPLE_EVENT(IgnoredUsersEvent, "m.ignored_user_list", QSet, ignored_users) } // namespace Quotient spectral/include/libQuotient/lib/events/callhangupevent.h0000644000175000000620000000232113566674122023707 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Marius Gripsgard * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "roomevent.h" namespace Quotient { class CallHangupEvent : public CallEventBase { public: DEFINE_EVENT_TYPEID("m.call.hangup", CallHangupEvent) explicit CallHangupEvent(const QJsonObject& obj); explicit CallHangupEvent(const QString& callId); }; REGISTER_EVENT_TYPE(CallHangupEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/callinviteevent.h0000644000175000000620000000276413566674122023736 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Marius Gripsgard * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "roomevent.h" namespace Quotient { class CallInviteEvent : public CallEventBase { public: DEFINE_EVENT_TYPEID("m.call.invite", CallInviteEvent) explicit CallInviteEvent(const QJsonObject& obj); explicit CallInviteEvent(const QString& callId, const int lifetime, const QString& sdp); int lifetime() const { return content("lifetime"_ls); } // FIXME: Omittable<>? QString sdp() const { return contentJson()["offer"_ls].toObject().value("sdp"_ls).toString(); } }; REGISTER_EVENT_TYPE(CallInviteEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/directchatevent.cpp0000644000175000000620000000302213566674122024235 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "directchatevent.h" #include using namespace Quotient; QMultiHash DirectChatEvent::usersToDirectChats() const { QMultiHash result; const auto& json = contentJson(); for (auto it = json.begin(); it != json.end(); ++it) { // Beware of range-for's over temporary returned from temporary // (see the bottom of // http://en.cppreference.com/w/cpp/language/range-for#Explanation) const auto roomIds = it.value().toArray(); for (const auto& roomIdValue : roomIds) result.insert(it.key(), roomIdValue.toString()); } return result; } spectral/include/libQuotient/lib/events/roommemberevent.cpp0000644000175000000620000000657113566674122024303 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "roommemberevent.h" #include "converters.h" #include "logging.h" #include static const std::array membershipStrings = { { QStringLiteral("invite"), QStringLiteral("join"), QStringLiteral("knock"), QStringLiteral("leave"), QStringLiteral("ban") } }; namespace Quotient { template <> struct JsonConverter { static MembershipType load(const QJsonValue& jv) { const auto& membershipString = jv.toString(); for (auto it = membershipStrings.begin(); it != membershipStrings.end(); ++it) if (membershipString == *it) return MembershipType(it - membershipStrings.begin()); qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString; return MembershipType::Undefined; } }; } // namespace Quotient using namespace Quotient; MemberEventContent::MemberEventContent(const QJsonObject& json) : membership(fromJson(json["membership"_ls])) , isDirect(json["is_direct"_ls].toBool()) , displayName(sanitized(json["displayname"_ls].toString())) , avatarUrl(json["avatar_url"_ls].toString()) {} void MemberEventContent::fillJson(QJsonObject* o) const { Q_ASSERT(o); Q_ASSERT_X(membership != MembershipType::Undefined, __FUNCTION__, "The key 'membership' must be explicit in MemberEventContent"); if (membership != MembershipType::Undefined) o->insert(QStringLiteral("membership"), membershipStrings[membership]); o->insert(QStringLiteral("displayname"), displayName); if (avatarUrl.isValid()) o->insert(QStringLiteral("avatar_url"), avatarUrl.toString()); } bool RoomMemberEvent::isInvite() const { return membership() == MembershipType::Invite && (!prevContent() || prevContent()->membership != membership()); } bool RoomMemberEvent::isJoin() const { return membership() == MembershipType::Join && (!prevContent() || prevContent()->membership != membership()); } bool RoomMemberEvent::isLeave() const { return membership() == MembershipType::Leave && prevContent() && prevContent()->membership != membership() && prevContent()->membership != MembershipType::Ban; } bool RoomMemberEvent::isRename() const { auto prevName = prevContent() ? prevContent()->displayName : QString(); return displayName() != prevName; } bool RoomMemberEvent::isAvatarUpdate() const { auto prevAvatarUrl = prevContent() ? prevContent()->avatarUrl : QUrl(); return avatarUrl() != prevAvatarUrl; } spectral/include/libQuotient/lib/events/directchatevent.h0000644000175000000620000000234213566674122023706 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "event.h" namespace Quotient { class DirectChatEvent : public Event { public: DEFINE_EVENT_TYPEID("m.direct", DirectChatEvent) explicit DirectChatEvent(const QJsonObject& obj) : Event(typeId(), obj) {} QMultiHash usersToDirectChats() const; }; REGISTER_EVENT_TYPE(DirectChatEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/callinviteevent.cpp0000644000175000000620000000401613566674122024261 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Marius Gripsgard * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "callinviteevent.h" #include "event.h" #include "logging.h" #include /* m.call.invite { "age": 242352, "content": { "call_id": "12345", "lifetime": 60000, "offer": { "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", "type": "offer" }, "version": 0 }, "event_id": "$WLGTSEFSEF:localhost", "origin_server_ts": 1431961217939, "room_id": "!Cuyf34gef24t:localhost", "sender": "@example:localhost", "type": "m.call.invite" } */ using namespace Quotient; CallInviteEvent::CallInviteEvent(const QJsonObject& obj) : CallEventBase(typeId(), obj) { qCDebug(EVENTS) << "Call Invite event"; } CallInviteEvent::CallInviteEvent(const QString& callId, const int lifetime, const QString& sdp) : CallEventBase( typeId(), matrixTypeId(), callId, lifetime, { { QStringLiteral("lifetime"), lifetime }, { QStringLiteral("offer"), QJsonObject { { QStringLiteral("type"), QStringLiteral("offer") }, { QStringLiteral("sdp"), sdp } } } }) {} spectral/include/libQuotient/lib/events/eventloader.h0000644000175000000620000000624613566674122023051 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "stateevent.h" namespace Quotient { namespace _impl { template static inline auto loadEvent(const QJsonObject& json, const QString& matrixType) { if (auto e = EventFactory::make(json, matrixType)) return e; return makeEvent(unknownEventTypeId(), json); } } // namespace _impl /*! Create an event with proper type from a JSON object * * Use this factory template to detect the type from the JSON object * contents (the detected event type should derive from the template * parameter type) and create an event object of that type. */ template inline event_ptr_tt loadEvent(const QJsonObject& fullJson) { return _impl::loadEvent(fullJson, fullJson[TypeKeyL].toString()); } /*! Create an event from a type string and content JSON * * Use this factory template to resolve the C++ type from the Matrix * type string in \p matrixType and create an event of that type that has * its content part set to \p content. */ template inline event_ptr_tt loadEvent(const QString& matrixType, const QJsonObject& content) { return _impl::loadEvent(basicEventJson(matrixType, content), matrixType); } /*! Create a state event from a type string, content JSON and state key * * Use this factory to resolve the C++ type from the Matrix type string * in \p matrixType and create a state event of that type with content part * set to \p content and state key set to \p stateKey (empty by default). */ inline StateEventPtr loadStateEvent(const QString& matrixType, const QJsonObject& content, const QString& stateKey = {}) { return _impl::loadEvent( basicStateEventJson(matrixType, content, stateKey), matrixType); } template struct JsonConverter> { static auto load(const QJsonValue& jv) { return loadEvent(jv.toObject()); } static auto load(const QJsonDocument& jd) { return loadEvent(jd.object()); } }; } // namespace Quotient spectral/include/libQuotient/lib/events/roommessageevent.cpp0000644000175000000620000003135313566674122024454 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "roommessageevent.h" #include "logging.h" #include #include #include #include using namespace Quotient; using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; static const auto RelatesToKeyL = "m.relates_to"_ls; static const auto MsgTypeKeyL = "msgtype"_ls; static const auto FormattedBodyKeyL = "formatted_body"_ls; static const auto TextTypeKey = "m.text"; static const auto EmoteTypeKey = "m.emote"; static const auto NoticeTypeKey = "m.notice"; static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html"); template TypedBase* make(const QJsonObject& json) { return new ContentT(json); } template <> TypedBase* make(const QJsonObject& json) { return json.contains(FormattedBodyKeyL) || json.contains(RelatesToKeyL) ? new TextContent(json) : nullptr; } struct MsgTypeDesc { QString matrixType; MsgType enumType; TypedBase* (*maker)(const QJsonObject&); }; const std::vector msgTypes = { { TextTypeKey, MsgType::Text, make }, { EmoteTypeKey, MsgType::Emote, make }, { NoticeTypeKey, MsgType::Notice, make }, { QStringLiteral("m.image"), MsgType::Image, make }, { QStringLiteral("m.file"), MsgType::File, make }, { QStringLiteral("m.location"), MsgType::Location, make }, { QStringLiteral("m.video"), MsgType::Video, make }, { QStringLiteral("m.audio"), MsgType::Audio, make } }; QString msgTypeToJson(MsgType enumType) { auto it = std::find_if(msgTypes.begin(), msgTypes.end(), [=](const MsgTypeDesc& mtd) { return mtd.enumType == enumType; }); if (it != msgTypes.end()) return it->matrixType; return {}; } MsgType jsonToMsgType(const QString& matrixType) { auto it = std::find_if(msgTypes.begin(), msgTypes.end(), [=](const MsgTypeDesc& mtd) { return mtd.matrixType == matrixType; }); if (it != msgTypes.end()) return it->enumType; return MsgType::Unknown; } QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) { auto json = content ? content->toJson() : QJsonObject(); if (json.contains(RelatesToKeyL)) { if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey && jsonMsgType != EmoteTypeKey) { json.remove(RelatesToKeyL); qCWarning(EVENTS) << RelatesToKeyL << "cannot be used in" << jsonMsgType << "messages; the relation has been stripped off"; } else { // After the above, we know for sure that the content is TextContent // and that its RelatesTo structure is not omitted auto* textContent = static_cast(content); if (textContent->relatesTo->type == RelatesTo::ReplacementTypeId()) { auto newContentJson = json.take("m.new_content"_ls).toObject(); newContentJson.insert(BodyKey, plainBody); newContentJson.insert(TypeKey, jsonMsgType); json.insert(QStringLiteral("m.new_content"), newContentJson); json[BodyKeyL] = "* " + plainBody; } } } json.insert(QStringLiteral("msgtype"), jsonMsgType); json.insert(QStringLiteral("body"), plainBody); return json; } RoomMessageEvent::RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) : RoomEvent(typeId(), matrixTypeId(), assembleContentJson(plainBody, jsonMsgType, content)) , _content(content) {} RoomMessageEvent::RoomMessageEvent(const QString& plainBody, MsgType msgType, TypedBase* content) : RoomMessageEvent(plainBody, msgTypeToJson(msgType), content) {} TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) { auto filePath = file.absoluteFilePath(); auto localUrl = QUrl::fromLocalFile(filePath); auto mimeType = QMimeDatabase().mimeTypeForFile(file); if (!asGenericFile) { auto mimeTypeName = mimeType.name(); if (mimeTypeName.startsWith("image/")) return new ImageContent(localUrl, file.size(), mimeType, QImageReader(filePath).size(), file.fileName()); // duration can only be obtained asynchronously and can only be reliably // done by starting to play the file. Left for a future implementation. if (mimeTypeName.startsWith("video/")) return new VideoContent(localUrl, file.size(), mimeType, QMediaResource(localUrl).resolution(), file.fileName()); if (mimeTypeName.startsWith("audio/")) return new AudioContent(localUrl, file.size(), mimeType, file.fileName()); } return new FileContent(localUrl, file.size(), mimeType, file.fileName()); } RoomMessageEvent::RoomMessageEvent(const QString& plainBody, const QFileInfo& file, bool asGenericFile) : RoomMessageEvent(plainBody, asGenericFile ? QStringLiteral("m.file") : rawMsgTypeForFile(file), contentFromFile(file, asGenericFile)) {} RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj), _content(nullptr) { if (isRedacted()) return; const QJsonObject content = contentJson(); if (content.contains(MsgTypeKeyL) && content.contains(BodyKeyL)) { auto msgtype = content[MsgTypeKeyL].toString(); bool msgTypeFound = false; for (const auto& mt : msgTypes) if (mt.matrixType == msgtype) { _content.reset(mt.maker(content)); msgTypeFound = true; } if (!msgTypeFound) { qCWarning(EVENTS) << "RoomMessageEvent: unknown msg_type," << " full content dump follows"; qCWarning(EVENTS) << formatJson << content; } } else { qCWarning(EVENTS) << "No body or msgtype in room message event"; qCWarning(EVENTS) << formatJson << obj; } } RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const { return jsonToMsgType(rawMsgtype()); } QString RoomMessageEvent::rawMsgtype() const { return contentJson()[MsgTypeKeyL].toString(); } QString RoomMessageEvent::plainBody() const { return contentJson()[BodyKeyL].toString(); } QMimeType RoomMessageEvent::mimeType() const { static const auto PlainTextMimeType = QMimeDatabase().mimeTypeForName("text/plain"); return _content ? _content->type() : PlainTextMimeType; } bool RoomMessageEvent::hasTextContent() const { return !content() || (msgtype() == MsgType::Text || msgtype() == MsgType::Emote || msgtype() == MsgType::Notice); } bool RoomMessageEvent::hasFileContent() const { return content() && content()->fileInfo(); } bool RoomMessageEvent::hasThumbnail() const { return content() && content()->thumbnailInfo(); } QString RoomMessageEvent::replacedEvent() const { if (!content() || !hasTextContent()) return {}; const auto& rel = static_cast(content())->relatesTo; return !rel.omitted() && rel->type == RelatesTo::ReplacementTypeId() ? rel->eventId : QString(); } QString rawMsgTypeForMimeType(const QMimeType& mimeType) { auto name = mimeType.name(); return name.startsWith("image/") ? QStringLiteral("m.image") : name.startsWith("video/") ? QStringLiteral("m.video") : name.startsWith("audio/") ? QStringLiteral("m.audio") : QStringLiteral("m.file"); } QString RoomMessageEvent::rawMsgTypeForUrl(const QUrl& url) { return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForUrl(url)); } QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi) { return rawMsgTypeForMimeType(QMimeDatabase().mimeTypeForFile(fi)); } TextContent::TextContent(const QString& text, const QString& contentType, Omittable relatesTo) : mimeType(QMimeDatabase().mimeTypeForName(contentType)) , body(text) , relatesTo(std::move(relatesTo)) { if (contentType == HtmlContentTypeId) mimeType = QMimeDatabase().mimeTypeForName("text/html"); } namespace Quotient { // Overload the default fromJson<> logic that defined in converters.h // as we want template <> Omittable fromJson(const QJsonValue& jv) { const auto jo = jv.toObject(); if (jo.isEmpty()) return none; const auto replyJson = jo.value(RelatesTo::ReplyTypeId()).toObject(); if (!replyJson.isEmpty()) return replyTo(fromJson(replyJson[EventIdKeyL])); return RelatesTo { jo.value("rel_type"_ls).toString(), jo.value(EventIdKeyL).toString() }; } } // namespace Quotient TextContent::TextContent(const QJsonObject& json) : relatesTo(fromJson>(json[RelatesToKeyL])) { QMimeDatabase db; static const auto PlainTextMimeType = db.mimeTypeForName("text/plain"); static const auto HtmlMimeType = db.mimeTypeForName("text/html"); const auto actualJson = relatesTo.omitted() || relatesTo->type != RelatesTo::ReplacementTypeId() ? json : json.value("m.new_content"_ls).toObject(); // Special-casing the custom matrix.org's (actually, Riot's) way // of sending HTML messages. if (actualJson["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; body = actualJson[FormattedBodyKeyL].toString(); } else { // Falling back to plain text, as there's no standard way to describe // rich text in messages. mimeType = PlainTextMimeType; body = actualJson[BodyKeyL].toString(); } } void TextContent::fillJson(QJsonObject* json) const { static const auto FormatKey = QStringLiteral("format"); static const auto FormattedBodyKey = QStringLiteral("formatted_body"); Q_ASSERT(json); if (mimeType.inherits("text/html")) { json->insert(FormatKey, HtmlContentTypeId); json->insert(FormattedBodyKey, body); } if (!relatesTo.omitted()) { json->insert(QStringLiteral("m.relates_to"), QJsonObject { { relatesTo->type, relatesTo->eventId } }); if (relatesTo->type == RelatesTo::ReplacementTypeId()) { QJsonObject newContentJson; if (mimeType.inherits("text/html")) { json->insert(FormatKey, HtmlContentTypeId); json->insert(FormattedBodyKey, body); } json->insert(QStringLiteral("m.new_content"), newContentJson); } } } LocationContent::LocationContent(const QString& geoUri, const Thumbnail& thumbnail) : geoUri(geoUri), thumbnail(thumbnail) {} LocationContent::LocationContent(const QJsonObject& json) : TypedBase(json) , geoUri(json["geo_uri"_ls].toString()) , thumbnail(json["info"_ls].toObject()) {} QMimeType LocationContent::type() const { return QMimeDatabase().mimeTypeForData(geoUri.toLatin1()); } void LocationContent::fillJson(QJsonObject* o) const { Q_ASSERT(o); o->insert(QStringLiteral("geo_uri"), geoUri); o->insert(QStringLiteral("info"), toInfoJson(thumbnail)); } spectral/include/libQuotient/lib/events/roommessageevent.h0000644000175000000620000001613513566674122024122 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "eventcontent.h" #include "roomevent.h" class QFileInfo; namespace Quotient { namespace MessageEventContent = EventContent; // Back-compatibility /** * The event class corresponding to m.room.message events */ class RoomMessageEvent : public RoomEvent { Q_GADGET Q_PROPERTY(QString msgType READ rawMsgtype CONSTANT) Q_PROPERTY(QString plainBody READ plainBody CONSTANT) Q_PROPERTY(QMimeType mimeType READ mimeType STORED false CONSTANT) Q_PROPERTY(const EventContent::TypedBase* content READ content CONSTANT) public: DEFINE_EVENT_TYPEID("m.room.message", RoomMessageEvent) enum class MsgType { Text, Emote, Notice, Image, File, Location, Video, Audio, Unknown }; RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, EventContent::TypedBase* content = nullptr); explicit RoomMessageEvent(const QString& plainBody, MsgType msgType = MsgType::Text, EventContent::TypedBase* content = nullptr); explicit RoomMessageEvent(const QString& plainBody, const QFileInfo& file, bool asGenericFile = false); explicit RoomMessageEvent(const QJsonObject& obj); MsgType msgtype() const; QString rawMsgtype() const; QString plainBody() const; const EventContent::TypedBase* content() const { return _content.data(); } template void editContent(VisitorT&& visitor) { visitor(*_content); editJson()[ContentKeyL] = assembleContentJson(plainBody(), rawMsgtype(), _content.data()); } QMimeType mimeType() const; bool hasTextContent() const; bool hasFileContent() const; bool hasThumbnail() const; QString replacedEvent() const; static QString rawMsgTypeForUrl(const QUrl& url); static QString rawMsgTypeForFile(const QFileInfo& fi); private: QScopedPointer _content; // FIXME: should it really be static? static QJsonObject assembleContentJson(const QString& plainBody, const QString& jsonMsgType, EventContent::TypedBase* content); Q_ENUM(MsgType) }; REGISTER_EVENT_TYPE(RoomMessageEvent) using MessageEventType = RoomMessageEvent::MsgType; namespace EventContent { // Additional event content types struct RelatesTo { static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; } static constexpr const char* ReplacementTypeId() { return "m.replace"; } QString type; // The only supported relation so far QString eventId; }; inline RelatesTo replyTo(QString eventId) { return { RelatesTo::ReplyTypeId(), std::move(eventId) }; } /** * Rich text content for m.text, m.emote, m.notice * * Available fields: mimeType, body. The body can be either rich text * or plain text, depending on what mimeType specifies. */ class TextContent : public TypedBase { public: TextContent(const QString& text, const QString& contentType, Omittable relatesTo = none); explicit TextContent(const QJsonObject& json); QMimeType type() const override { return mimeType; } QMimeType mimeType; QString body; Omittable relatesTo; protected: void fillJson(QJsonObject* json) const override; }; /** * Content class for m.location * * Available fields: * - corresponding to the top-level JSON: * - geoUri ("geo_uri" in JSON) * - corresponding to the "info" subobject: * - thumbnail.url ("thumbnail_url" in JSON) * - corresponding to the "info/thumbnail_info" subobject: * - thumbnail.payloadSize * - thumbnail.mimeType * - thumbnail.imageSize */ class LocationContent : public TypedBase { public: LocationContent(const QString& geoUri, const Thumbnail& thumbnail = {}); explicit LocationContent(const QJsonObject& json); QMimeType type() const override; public: QString geoUri; Thumbnail thumbnail; protected: void fillJson(QJsonObject* o) const override; }; /** * A base class for info types that include duration: audio and video */ template class PlayableContent : public ContentT { public: using ContentT::ContentT; PlayableContent(const QJsonObject& json) : ContentT(json) , duration(ContentT::originalInfoJson["duration"_ls].toInt()) {} protected: void fillJson(QJsonObject* json) const override { ContentT::fillJson(json); auto infoJson = json->take("info"_ls).toObject(); infoJson.insert(QStringLiteral("duration"), duration); json->insert(QStringLiteral("info"), infoJson); } public: int duration; }; /** * Content class for m.video * * Available fields: * - corresponding to the top-level JSON: * - url * - filename (extension to the CS API spec) * - corresponding to the "info" subobject: * - payloadSize ("size" in JSON) * - mimeType ("mimetype" in JSON) * - duration * - imageSize (QSize for a combination of "h" and "w" in JSON) * - thumbnail.url ("thumbnail_url" in JSON) * - corresponding to the "info/thumbnail_info" subobject: contents of * thumbnail field, in the same vein as for "info": * - payloadSize * - mimeType * - imageSize */ using VideoContent = PlayableContent>; /** * Content class for m.audio * * Available fields: * - corresponding to the top-level JSON: * - url * - filename (extension to the CS API spec) * - corresponding to the "info" subobject: * - payloadSize ("size" in JSON) * - mimeType ("mimetype" in JSON) * - duration */ using AudioContent = PlayableContent>; } // namespace EventContent } // namespace Quotient spectral/include/libQuotient/lib/events/receiptevent.h0000644000175000000620000000302313566674122023224 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2016 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "event.h" #include #include namespace Quotient { struct Receipt { QString userId; QDateTime timestamp; }; struct ReceiptsForEvent { QString evtId; QVector receipts; }; using EventsWithReceipts = QVector; class ReceiptEvent : public Event { public: DEFINE_EVENT_TYPEID("m.receipt", ReceiptEvent) explicit ReceiptEvent(const QJsonObject& obj); const EventsWithReceipts& eventsWithReceipts() const { return _eventsWithReceipts; } private: EventsWithReceipts _eventsWithReceipts; }; REGISTER_EVENT_TYPE(ReceiptEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/callhangupevent.cpp0000644000175000000620000000305313566674122024245 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Marius Gripsgard * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "callhangupevent.h" #include "event.h" #include "logging.h" #include /* m.call.hangup { "age": 242352, "content": { "call_id": "12345", "version": 0 }, "event_id": "$WLGTSEFSEF:localhost", "origin_server_ts": 1431961217939, "room_id": "!Cuyf34gef24t:localhost", "sender": "@example:localhost", "type": "m.call.hangup" } */ using namespace Quotient; CallHangupEvent::CallHangupEvent(const QJsonObject& obj) : CallEventBase(typeId(), obj) { qCDebug(EVENTS) << "Call Hangup event"; } CallHangupEvent::CallHangupEvent(const QString& callId) : CallEventBase(typeId(), matrixTypeId(), callId, 0) {} spectral/include/libQuotient/lib/events/encryptedevent.h0000644000175000000620000000466213566674122023600 0ustar dilingerstaff#pragma once #include "e2ee.h" #include "roomevent.h" namespace Quotient { class Room; /* * While the specification states: * * "This event type is used when sending encrypted events. * It can be used either within a room * (in which case it will have all of the Room Event fields), * or as a to-device event." * "The encrypted payload can contain any message event." * https://matrix.org/docs/spec/client_server/latest#id493 * * -- for most of the cases the message event is the room message event. * And even for the to-device events the context is for the room. * * So, to simplify integration to the timeline, EncryptedEvent is a RoomEvent * inheritor. Strictly speaking though, it's not always a RoomEvent, but an Event * in general. It's possible, because RoomEvent interface is similar to Event's * one and doesn't add new restrictions, just provides additional features. */ class EncryptedEvent : public RoomEvent { Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.encrypted", EncryptedEvent) /* In case with Olm, the encrypted content of the event is * a map from the recipient Curve25519 identity key to ciphertext * information */ explicit EncryptedEvent(const QJsonObject& ciphertext, const QString& senderKey); /* In case with Megolm, device_id and session_id are required */ explicit EncryptedEvent(QByteArray ciphertext, const QString& senderKey, const QString& deviceId, const QString& sessionId); explicit EncryptedEvent(const QJsonObject& obj); QString algorithm() const { QString algo = content(AlgorithmKeyL); if (!SupportedAlgorithms.contains(algo)) { qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo << "is not supported"; } return algo; } QByteArray ciphertext() const { return content(CiphertextKeyL).toLatin1(); } QJsonObject ciphertext(const QString& identityKey) const { return content(CiphertextKeyL).value(identityKey).toObject(); } QString senderKey() const { return content(SenderKeyKeyL); } /* device_id and session_id are required with Megolm */ QString deviceId() const { return content(DeviceIdKeyL); } QString sessionId() const { return content(SessionIdKeyL); } }; REGISTER_EVENT_TYPE(EncryptedEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/redactionevent.h0000644000175000000620000000255013566674122023545 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "roomevent.h" namespace Quotient { class RedactionEvent : public RoomEvent { public: DEFINE_EVENT_TYPEID("m.room.redaction", RedactionEvent) explicit RedactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) {} QString redactedEvent() const { return fullJson()["redacts"_ls].toString(); } QString reason() const { return contentJson()["reason"_ls].toString(); } }; REGISTER_EVENT_TYPE(RedactionEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/eventcontent.cpp0000644000175000000620000000661213566674122023605 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "eventcontent.h" #include "converters.h" #include "util.h" #include using namespace Quotient::EventContent; QJsonObject Base::toJson() const { QJsonObject o; fillJson(&o); return o; } FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType, const QString& originalFilename) : mimeType(mimeType) , url(u) , payloadSize(payloadSize) , originalName(originalFilename) {} FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename) : originalInfoJson(infoJson) , mimeType( QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) , url(u) , payloadSize(fromJson(infoJson["size"_ls])) , originalName(originalFilename) { if (!mimeType.isValid()) mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); } bool FileInfo::isValid() const { return url.scheme() == "mxc" && (url.authority() + url.path()).count('/') == 1; } void FileInfo::fillInfoJson(QJsonObject* infoJson) const { Q_ASSERT(infoJson); if (payloadSize != -1) infoJson->insert(QStringLiteral("size"), payloadSize); if (mimeType.isValid()) infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); } ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType, const QSize& imageSize, const QString& originalFilename) : FileInfo(u, fileSize, mimeType, originalFilename), imageSize(imageSize) {} ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename) : FileInfo(u, infoJson, originalFilename) , imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt()) {} void ImageInfo::fillInfoJson(QJsonObject* infoJson) const { FileInfo::fillInfoJson(infoJson); if (imageSize.width() != -1) infoJson->insert(QStringLiteral("w"), imageSize.width()); if (imageSize.height() != -1) infoJson->insert(QStringLiteral("h"), imageSize.height()); } Thumbnail::Thumbnail(const QJsonObject& infoJson) : ImageInfo(infoJson["thumbnail_url"_ls].toString(), infoJson["thumbnail_info"_ls].toObject()) {} void Thumbnail::fillInfoJson(QJsonObject* infoJson) const { if (url.isValid()) infoJson->insert(QStringLiteral("thumbnail_url"), url.toString()); if (!imageSize.isEmpty()) infoJson->insert(QStringLiteral("thumbnail_info"), toInfoJson(*this)); } spectral/include/libQuotient/lib/events/simplestateevents.h0000644000175000000620000000773113566674122024320 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "stateevent.h" namespace Quotient { namespace EventContent { template class SimpleContent { public: using value_type = T; // The constructor is templated to enable perfect forwarding template SimpleContent(QString keyName, TT&& value) : value(std::forward(value)), key(std::move(keyName)) {} SimpleContent(const QJsonObject& json, QString keyName) : value(fromJson(json[keyName])), key(std::move(keyName)) {} QJsonObject toJson() const { return { { key, Quotient::toJson(value) } }; } public: T value; protected: QString key; }; } // namespace EventContent #define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ class _Name : public StateEvent> { \ public: \ using value_type = content_type::value_type; \ DEFINE_EVENT_TYPEID(_TypeId, _Name) \ explicit _Name() : _Name(value_type()) {} \ template \ explicit _Name(T&& value) \ : StateEvent(typeId(), matrixTypeId(), QString(), \ QStringLiteral(#_ContentKey), std::forward(value)) \ {} \ explicit _Name(QJsonObject obj) \ : StateEvent(typeId(), std::move(obj), \ QStringLiteral(#_ContentKey)) \ {} \ auto _ContentKey() const { return content().value; } \ }; \ REGISTER_EVENT_TYPE(_Name) \ // End of macro DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) DEFINE_SIMPLE_STATE_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias", QString, alias) DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic) class RoomAliasesEvent : public StateEvent> { public: DEFINE_EVENT_TYPEID("m.room.aliases", RoomAliasesEvent) explicit RoomAliasesEvent(const QJsonObject& obj) : StateEvent(typeId(), obj, QStringLiteral("aliases")) {} RoomAliasesEvent(const QString& server, const QStringList& aliases) : StateEvent(typeId(), matrixTypeId(), server, QStringLiteral("aliases"), aliases) {} QString server() const { return stateKey(); } QStringList aliases() const { return content().value; } }; REGISTER_EVENT_TYPE(RoomAliasesEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/typingevent.h0000644000175000000620000000230313566674122023103 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "event.h" namespace Quotient { class TypingEvent : public Event { public: DEFINE_EVENT_TYPEID("m.typing", TypingEvent) TypingEvent(const QJsonObject& obj); const QStringList& users() const { return _users; } private: QStringList _users; }; REGISTER_EVENT_TYPE(TypingEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/reactionevent.h0000644000175000000620000000466013566674122023405 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2019 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "roomevent.h" namespace Quotient { struct EventRelation { using reltypeid_t = const char*; static constexpr reltypeid_t Reply() { return "m.in_reply_to"; } static constexpr reltypeid_t Annotation() { return "m.annotation"; } static constexpr reltypeid_t Replacement() { return "m.replace"; } QString type; QString eventId; QString key = {}; // Only used for m.annotation for now static EventRelation replyTo(QString eventId) { return { Reply(), std::move(eventId) }; } static EventRelation annotate(QString eventId, QString key) { return { Annotation(), std::move(eventId), std::move(key) }; } static EventRelation replace(QString eventId) { return { Replacement(), std::move(eventId) }; } }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const EventRelation& pod); static void fillFrom(const QJsonObject& jo, EventRelation& pod); }; class ReactionEvent : public RoomEvent { public: DEFINE_EVENT_TYPEID("m.reaction", ReactionEvent) explicit ReactionEvent(const EventRelation& value) : RoomEvent(typeId(), matrixTypeId(), { { QStringLiteral("m.relates_to"), toJson(value) } }) {} explicit ReactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) {} EventRelation relation() const { return content(QStringLiteral("m.relates_to")); } private: EventRelation _relation; }; REGISTER_EVENT_TYPE(ReactionEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/roomtombstoneevent.cpp0000644000175000000620000000221713566674122025037 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2019 QMatrixClient project * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "roomtombstoneevent.h" using namespace Quotient; QString RoomTombstoneEvent::serverMessage() const { return fromJson(contentJson()["body"_ls]); } QString RoomTombstoneEvent::successorRoomId() const { return fromJson(contentJson()["replacement_room"_ls]); } spectral/include/libQuotient/lib/events/callcandidatesevent.h0000644000175000000620000000310713566674122024527 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Marius Gripsgard * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "roomevent.h" namespace Quotient { class CallCandidatesEvent : public CallEventBase { public: DEFINE_EVENT_TYPEID("m.call.candidates", CallCandidatesEvent) explicit CallCandidatesEvent(const QJsonObject& obj) : CallEventBase(typeId(), obj) {} explicit CallCandidatesEvent(const QString& callId, const QJsonArray& candidates) : CallEventBase(typeId(), matrixTypeId(), callId, 0, { { QStringLiteral("candidates"), candidates } }) {} QJsonArray candidates() const { return content("candidates"_ls); } }; REGISTER_EVENT_TYPE(CallCandidatesEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/roomevent.h0000644000175000000620000000760713566674122022561 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "event.h" #include namespace Quotient { class RedactionEvent; /** This class corresponds to m.room.* events */ class RoomEvent : public Event { Q_GADGET Q_PROPERTY(QString id READ id) Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT) Q_PROPERTY(QString roomId READ roomId CONSTANT) Q_PROPERTY(QString senderId READ senderId CONSTANT) Q_PROPERTY(QString redactionReason READ redactionReason) Q_PROPERTY(bool isRedacted READ isRedacted) Q_PROPERTY(QString transactionId READ transactionId WRITE setTransactionId) public: using factory_t = EventFactory; // RedactionEvent is an incomplete type here so we cannot inline // constructors and destructors and we cannot use 'using'. RoomEvent(Type type, event_mtype_t matrixType, const QJsonObject& contentJson = {}); RoomEvent(Type type, const QJsonObject& json); ~RoomEvent() override; QString id() const; QDateTime timestamp() const; QString roomId() const; QString senderId() const; bool isReplaced() const; QString replacedBy() const; bool isRedacted() const { return bool(_redactedBecause); } const event_ptr_tt& redactedBecause() const { return _redactedBecause; } QString redactionReason() const; QString transactionId() const; QString stateKey() const; void setRoomId(const QString& roomId); void setSender(const QString& senderId); /** * Sets the transaction id for locally created events. This should be * done before the event is exposed to any code using the respective * Q_PROPERTY. * * \param txnId - transaction id, normally obtained from * Connection::generateTxnId() */ void setTransactionId(const QString& txnId); /** * Sets event id for locally created events * * When a new event is created locally, it has no server id yet. * This function allows to add the id once the confirmation from * the server is received. There should be no id set previously * in the event. It's the responsibility of the code calling addId() * to notify clients that use Q_PROPERTY(id) about its change */ void addId(const QString& newId); private: event_ptr_tt _redactedBecause; }; using RoomEventPtr = event_ptr_tt; using RoomEvents = EventsArray; using RoomEventsRange = Range; class CallEventBase : public RoomEvent { public: CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, int version, const QJsonObject& contentJson = {}); CallEventBase(Type type, const QJsonObject& json); ~CallEventBase() override = default; bool isCallEvent() const override { return true; } QString callId() const { return content("call_id"_ls); } int version() const { return content("version"_ls); } }; } // namespace Quotient Q_DECLARE_METATYPE(Quotient::RoomEvent*) Q_DECLARE_METATYPE(const Quotient::RoomEvent*) spectral/include/libQuotient/lib/events/eventcontent.h0000644000175000000620000002377213566674122023260 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once // This file contains generic event content definitions, applicable to room // message events as well as other events (e.g., avatars). #include #include #include #include #include namespace Quotient { namespace EventContent { /** * A base class for all content types that can be stored * in a RoomMessageEvent * * Each content type class should have a constructor taking * a QJsonObject and override fillJson() with an implementation * that will fill the target QJsonObject with stored values. It is * assumed but not required that a content object can also be created * from plain data. */ class Base { public: explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {} virtual ~Base() = default; // FIXME: make toJson() from converters.* work on base classes QJsonObject toJson() const; public: QJsonObject originalJson; protected: Base(const Base&) = default; Base(Base&&) = default; virtual void fillJson(QJsonObject* o) const = 0; }; // The below structures fairly follow CS spec 11.2.1.6. The overall // set of attributes for each content types is a superset of the spec // but specific aggregation structure is altered. See doc comments to // each type for the list of available attributes. // A quick classes inheritance structure follows: // FileInfo // FileContent : UrlBasedContent // AudioContent : UrlBasedContent // ImageInfo : FileInfo + imageSize attribute // ImageContent : UrlBasedContent // VideoContent : UrlBasedContent /** * A base/mixin class for structures representing an "info" object for * some content types. These include most attachment types currently in * the CS API spec. * * In order to use it in a content class, derive both from TypedBase * (or Base) and from FileInfo (or its derivative, such as \p ImageInfo) * and call fillInfoJson() to fill the "info" subobject. Make sure * to pass an "info" part of JSON to FileInfo constructor, not the whole * JSON content, as well as contents of "url" (or a similar key) and * optionally "filename" node from the main JSON content. Assuming you * don't do unusual things, you should use \p UrlBasedContent<> instead * of doing multiple inheritance and overriding Base::fillJson() by hand. * * This class is not polymorphic. */ class FileInfo { public: explicit FileInfo(const QUrl& u, qint64 payloadSize = -1, const QMimeType& mimeType = {}, const QString& originalFilename = {}); FileInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename = {}); bool isValid() const; void fillInfoJson(QJsonObject* infoJson) const; /** * \brief Extract media id from the URL * * This can be used, e.g., to construct a QML-facing image:// * URI as follows: * \code "image://provider/" + info.mediaId() \endcode */ QString mediaId() const { return url.authority() + url.path(); } public: QJsonObject originalInfoJson; QMimeType mimeType; QUrl url; qint64 payloadSize; QString originalName; }; template QJsonObject toInfoJson(const InfoT& info) { QJsonObject infoJson; info.fillInfoJson(&infoJson); return infoJson; } /** * A content info class for image content types: image, thumbnail, video */ class ImageInfo : public FileInfo { public: explicit ImageInfo(const QUrl& u, qint64 fileSize = -1, QMimeType mimeType = {}, const QSize& imageSize = {}, const QString& originalFilename = {}); ImageInfo(const QUrl& u, const QJsonObject& infoJson, const QString& originalFilename = {}); void fillInfoJson(QJsonObject* infoJson) const; public: QSize imageSize; }; /** * An auxiliary class for an info type that carries a thumbnail * * This class saves/loads a thumbnail to/from "info" subobject of * the JSON representation of event content; namely, * "info/thumbnail_url" and "info/thumbnail_info" fields are used. */ class Thumbnail : public ImageInfo { public: Thumbnail() : ImageInfo(QUrl()) {} // To allow empty thumbnails Thumbnail(const QJsonObject& infoJson); Thumbnail(const ImageInfo& info) : ImageInfo(info) {} using ImageInfo::ImageInfo; /** * Writes thumbnail information to "thumbnail_info" subobject * and thumbnail URL to "thumbnail_url" node inside "info". */ void fillInfoJson(QJsonObject* infoJson) const; }; class TypedBase : public Base { public: explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {} virtual QMimeType type() const = 0; virtual const FileInfo* fileInfo() const { return nullptr; } virtual FileInfo* fileInfo() { return nullptr; } virtual const Thumbnail* thumbnailInfo() const { return nullptr; } protected: using Base::Base; }; /** * A base class for content types that have a URL and additional info * * Types that derive from this class template take "url" and, * optionally, "filename" values from the top-level JSON object and * the rest of information from the "info" subobject, as defined by * the parameter type. * * \tparam InfoT base info class */ template class UrlBasedContent : public TypedBase, public InfoT { public: using InfoT::InfoT; explicit UrlBasedContent(const QJsonObject& json) : TypedBase(json) , InfoT(json["url"].toString(), json["info"].toObject(), json["filename"].toString()) { // A small hack to facilitate links creation in QML. originalJson.insert("mediaId", InfoT::mediaId()); } QMimeType type() const override { return InfoT::mimeType; } const FileInfo* fileInfo() const override { return this; } FileInfo* fileInfo() override { return this; } protected: void fillJson(QJsonObject* json) const override { Q_ASSERT(json); json->insert("url", InfoT::url.toString()); if (!InfoT::originalName.isEmpty()) json->insert("filename", InfoT::originalName); json->insert("info", toInfoJson(*this)); } }; template class UrlWithThumbnailContent : public UrlBasedContent { public: using UrlBasedContent::UrlBasedContent; explicit UrlWithThumbnailContent(const QJsonObject& json) : UrlBasedContent(json), thumbnail(InfoT::originalInfoJson) { // Another small hack, to simplify making a thumbnail link UrlBasedContent::originalJson.insert("thumbnailMediaId", thumbnail.mediaId()); } const Thumbnail* thumbnailInfo() const override { return &thumbnail; } public: Thumbnail thumbnail; protected: void fillJson(QJsonObject* json) const override { UrlBasedContent::fillJson(json); auto infoJson = json->take("info").toObject(); thumbnail.fillInfoJson(&infoJson); json->insert("info", infoJson); } }; /** * Content class for m.image * * Available fields: * - corresponding to the top-level JSON: * - url * - filename (extension to the spec) * - corresponding to the "info" subobject: * - payloadSize ("size" in JSON) * - mimeType ("mimetype" in JSON) * - imageSize (QSize for a combination of "h" and "w" in JSON) * - thumbnail.url ("thumbnail_url" in JSON) * - corresponding to the "info/thumbnail_info" subobject: contents of * thumbnail field, in the same vein as for the main image: * - payloadSize * - mimeType * - imageSize */ using ImageContent = UrlWithThumbnailContent; /** * Content class for m.file * * Available fields: * - corresponding to the top-level JSON: * - url * - filename * - corresponding to the "info" subobject: * - payloadSize ("size" in JSON) * - mimeType ("mimetype" in JSON) * - thumbnail.url ("thumbnail_url" in JSON) * - corresponding to the "info/thumbnail_info" subobject: * - thumbnail.payloadSize * - thumbnail.mimeType * - thumbnail.imageSize (QSize for "h" and "w" in JSON) */ using FileContent = UrlWithThumbnailContent; } // namespace EventContent } // namespace Quotient Q_DECLARE_METATYPE(const Quotient::EventContent::TypedBase*) spectral/include/libQuotient/lib/events/encryptedevent.cpp0000644000175000000620000000210013566674122024114 0ustar dilingerstaff#include "encryptedevent.h" #include "room.h" using namespace Quotient; using namespace QtOlm; EncryptedEvent::EncryptedEvent(const QJsonObject& ciphertext, const QString& senderKey) : RoomEvent(typeId(), matrixTypeId(), { { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey }, { CiphertextKeyL, ciphertext }, { SenderKeyKeyL, senderKey } }) {} EncryptedEvent::EncryptedEvent(QByteArray ciphertext, const QString& senderKey, const QString& deviceId, const QString& sessionId) : RoomEvent(typeId(), matrixTypeId(), { { AlgorithmKeyL, MegolmV1AesSha2AlgoKey }, { CiphertextKeyL, QString(ciphertext) }, { DeviceIdKeyL, deviceId }, { SenderKeyKeyL, senderKey }, { SessionIdKeyL, sessionId }, }) {} EncryptedEvent::EncryptedEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) { qCDebug(EVENTS) << "Encrypted event" << id(); } spectral/include/libQuotient/lib/events/event.cpp0000644000175000000620000000477313566674122022220 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "event.h" #include "logging.h" #include using namespace Quotient; event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId) { const auto id = get().eventTypes.size(); get().eventTypes.push_back(matrixTypeId); if (strncmp(matrixTypeId, "", 1) == 0) qDebug(EVENTS) << "Initialized unknown event type with id" << id; else qDebug(EVENTS) << "Initialized event type" << matrixTypeId << "with id" << id; return id; } QString EventTypeRegistry::getMatrixType(event_type_t typeId) { return typeId < get().eventTypes.size() ? get().eventTypes[typeId] : QString(); } Event::Event(Type type, const QJsonObject& json) : _type(type), _json(json) { if (!json.contains(ContentKeyL) && !json.value(UnsignedKeyL).toObject().contains(RedactedCauseKeyL)) { qCWarning(EVENTS) << "Event without 'content' node"; qCWarning(EVENTS) << formatJson << json; } } Event::Event(Type type, event_mtype_t matrixType, const QJsonObject& contentJson) : Event(type, basicEventJson(matrixType, contentJson)) {} Event::~Event() = default; QString Event::matrixType() const { return fullJson()[TypeKeyL].toString(); } QByteArray Event::originalJson() const { return QJsonDocument(_json).toJson(); } const QJsonObject Event::contentJson() const { return fullJson()[ContentKeyL].toObject(); } const QJsonObject Event::unsignedJson() const { return fullJson()[UnsignedKeyL].toObject(); } void Event::dumpTo(QDebug dbg) const { dbg << QJsonDocument(contentJson()).toJson(QJsonDocument::Compact); } spectral/include/libQuotient/lib/events/callanswerevent.cpp0000644000175000000620000000452313566674122024265 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Marius Gripsgard * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "callanswerevent.h" #include "event.h" #include "logging.h" #include /* m.call.answer { "age": 242352, "content": { "answer": { "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", "type": "answer" }, "call_id": "12345", "lifetime": 60000, "version": 0 }, "event_id": "$WLGTSEFSEF:localhost", "origin_server_ts": 1431961217939, "room_id": "!Cuyf34gef24t:localhost", "sender": "@example:localhost", "type": "m.call.answer" } */ using namespace Quotient; CallAnswerEvent::CallAnswerEvent(const QJsonObject& obj) : CallEventBase(typeId(), obj) { qCDebug(EVENTS) << "Call Answer event"; } CallAnswerEvent::CallAnswerEvent(const QString& callId, const int lifetime, const QString& sdp) : CallEventBase( typeId(), matrixTypeId(), callId, 0, { { QStringLiteral("lifetime"), lifetime }, { QStringLiteral("answer"), QJsonObject { { QStringLiteral("type"), QStringLiteral("answer") }, { QStringLiteral("sdp"), sdp } } } }) {} CallAnswerEvent::CallAnswerEvent(const QString& callId, const QString& sdp) : CallEventBase( typeId(), matrixTypeId(), callId, 0, { { QStringLiteral("answer"), QJsonObject { { QStringLiteral("type"), QStringLiteral("answer") }, { QStringLiteral("sdp"), sdp } } } }) {} spectral/include/libQuotient/lib/events/stateevent.cpp0000644000175000000620000000467313566674122023260 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "stateevent.h" using namespace Quotient; // Aside from the normal factory to instantiate StateEventBase inheritors // StateEventBase itself can be instantiated if there's a state_key JSON key // but the event type is unknown. [[maybe_unused]] static auto stateEventTypeInitialised = RoomEvent::factory_t::addMethod( [](const QJsonObject& json, const QString& matrixType) -> StateEventPtr { if (!json.contains(StateKeyKeyL)) return nullptr; if (auto e = StateEventBase::factory_t::make(json, matrixType)) return e; return makeEvent(unknownEventTypeId(), json); }); StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, const QString& stateKey, const QJsonObject& contentJson) : RoomEvent(type, basicStateEventJson(matrixType, contentJson, stateKey)) {} bool StateEventBase::repeatsState() const { const auto prevContentJson = unsignedJson().value(PrevContentKeyL); return fullJson().value(ContentKeyL) == prevContentJson; } QString StateEventBase::replacedState() const { return unsignedJson().value("replaces_state"_ls).toString(); } void StateEventBase::dumpTo(QDebug dbg) const { if (!stateKey().isEmpty()) dbg << '<' << stateKey() << "> "; if (unsignedJson().contains(PrevContentKeyL)) dbg << QJsonDocument(unsignedJson()[PrevContentKeyL].toObject()) .toJson(QJsonDocument::Compact) << " -> "; RoomEvent::dumpTo(dbg); } spectral/include/libQuotient/lib/events/roomcreateevent.h0000644000175000000620000000272713566674122023743 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2019 QMatrixClient project * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "stateevent.h" namespace Quotient { class RoomCreateEvent : public StateEventBase { public: DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent) explicit RoomCreateEvent() : StateEventBase(typeId(), matrixTypeId()) {} explicit RoomCreateEvent(const QJsonObject& obj) : StateEventBase(typeId(), obj) {} struct Predecessor { QString roomId; QString eventId; }; bool isFederated() const; QString version() const; Predecessor predecessor() const; bool isUpgrade() const; }; REGISTER_EVENT_TYPE(RoomCreateEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/roommemberevent.h0000644000175000000620000000715713566674122023751 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "eventcontent.h" #include "stateevent.h" namespace Quotient { class MemberEventContent : public EventContent::Base { public: enum MembershipType : size_t { Invite = 0, Join, Knock, Leave, Ban, Undefined }; explicit MemberEventContent(MembershipType mt = Join) : membership(mt) {} explicit MemberEventContent(const QJsonObject& json); MembershipType membership; bool isDirect = false; QString displayName; QUrl avatarUrl; protected: void fillJson(QJsonObject* o) const override; }; using MembershipType = MemberEventContent::MembershipType; class RoomMemberEvent : public StateEvent { Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent) using MembershipType = MemberEventContent::MembershipType; explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} [[deprecated("Use RoomMemberEvent(userId, contentArgs) " "instead")]] RoomMemberEvent(MemberEventContent&& c) : StateEvent(typeId(), matrixTypeId(), QString(), c) {} template RoomMemberEvent(const QString& userId, ArgTs&&... contentArgs) : StateEvent(typeId(), matrixTypeId(), userId, std::forward(contentArgs)...) {} /// A special constructor to create unknown RoomMemberEvents /** * This is needed in order to use RoomMemberEvent as a "base event * class" in cases like GetMembersByRoomJob when RoomMemberEvents * (rather than RoomEvents or StateEvents) are resolved from JSON. * For such cases loadEvent<> requires an underlying class to be * constructible with unknownTypeId() instead of its genuine id. * Don't use it directly. * \sa GetMembersByRoomJob, loadEvent, unknownTypeId */ RoomMemberEvent(Type type, const QJsonObject& fullJson) : StateEvent(type, fullJson) {} MembershipType membership() const { return content().membership; } QString userId() const { return fullJson()[StateKeyKeyL].toString(); } bool isDirect() const { return content().isDirect; } QString displayName() const { return content().displayName; } QUrl avatarUrl() const { return content().avatarUrl; } bool isInvite() const; bool isJoin() const; bool isLeave() const; bool isRename() const; bool isAvatarUpdate() const; private: Q_ENUM(MembershipType) }; template <> class EventFactory { public: static event_ptr_tt make(const QJsonObject& json, const QString&) { return makeEvent(json); } }; REGISTER_EVENT_TYPE(RoomMemberEvent) } // namespace Quotient spectral/include/libQuotient/lib/events/receiptevent.cpp0000644000175000000620000000442413566674122023565 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2016 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /* Example of a Receipt Event: { "content": { "$1435641916114394fHBLK:matrix.org": { "m.read": { "@rikj:jki.re": { "ts": 1436451550453 } } } }, "room_id": "!KpjVgQyZpzBwvMBsnT:matrix.org", "type": "m.receipt" } */ #include "receiptevent.h" #include "converters.h" #include "logging.h" using namespace Quotient; ReceiptEvent::ReceiptEvent(const QJsonObject& obj) : Event(typeId(), obj) { const auto& contents = contentJson(); _eventsWithReceipts.reserve(contents.size()); for (auto eventIt = contents.begin(); eventIt != contents.end(); ++eventIt) { if (eventIt.key().isEmpty()) { qCWarning(EPHEMERAL) << "ReceiptEvent has an empty event id, skipping"; qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << contents; continue; } const QJsonObject reads = eventIt.value().toObject().value("m.read"_ls).toObject(); QVector receipts; receipts.reserve(reads.size()); for (auto userIt = reads.begin(); userIt != reads.end(); ++userIt) { const QJsonObject user = userIt.value().toObject(); receipts.push_back( { userIt.key(), fromJson(user["ts"_ls]) }); } _eventsWithReceipts.push_back({ eventIt.key(), std::move(receipts) }); } } spectral/include/libQuotient/lib/events/encryptionevent.cpp0000644000175000000620000000316413566674122024324 0ustar dilingerstaff// // Created by rusakov on 26/09/2017. // Contributed by andreev on 27/06/2019. // #include "encryptionevent.h" #include "converters.h" #include "e2ee.h" #include "logging.h" #include static const std::array encryptionStrings = { { Quotient::MegolmV1AesSha2AlgoKey } }; namespace Quotient { template <> struct JsonConverter { static EncryptionType load(const QJsonValue& jv) { const auto& encryptionString = jv.toString(); for (auto it = encryptionStrings.begin(); it != encryptionStrings.end(); ++it) if (encryptionString == *it) return EncryptionType(it - encryptionStrings.begin()); qCWarning(EVENTS) << "Unknown EncryptionType: " << encryptionString; return EncryptionType::Undefined; } }; } // namespace Quotient using namespace Quotient; EncryptionEventContent::EncryptionEventContent(const QJsonObject& json) : encryption(fromJson(json["algorithm"_ls])) , algorithm(sanitized(json[AlgorithmKeyL].toString())) , rotationPeriodMs(json[RotationPeriodMsKeyL].toInt(604800000)) , rotationPeriodMsgs(json[RotationPeriodMsgsKeyL].toInt(100)) {} void EncryptionEventContent::fillJson(QJsonObject* o) const { Q_ASSERT(o); Q_ASSERT_X( encryption != EncryptionType::Undefined, __FUNCTION__, "The key 'algorithm' must be explicit in EncryptionEventContent"); if (encryption != EncryptionType::Undefined) o->insert(AlgorithmKey, algorithm); o->insert(RotationPeriodMsKey, rotationPeriodMs); o->insert(RotationPeriodMsgsKey, rotationPeriodMsgs); } spectral/include/libQuotient/lib/events/typingevent.cpp0000644000175000000620000000223413566674122023441 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "typingevent.h" #include using namespace Quotient; TypingEvent::TypingEvent(const QJsonObject& obj) : Event(typeId(), obj) { const auto& array = contentJson()["user_ids"_ls].toArray(); for (const auto& user : array) _users.push_back(user.toString()); } spectral/include/libQuotient/lib/application-service/0002755000175000000620000000000013566674122023017 5ustar dilingerstaffspectral/include/libQuotient/lib/application-service/definitions/0002755000175000000620000000000013566674122025332 5ustar dilingerstaffspectral/include/libQuotient/lib/application-service/definitions/location.cpp0000644000175000000620000000144313566674122027646 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "location.h" using namespace Quotient; void JsonObjectConverter::dumpTo( QJsonObject& jo, const ThirdPartyLocation& pod) { addParam<>(jo, QStringLiteral("alias"), pod.alias); addParam<>(jo, QStringLiteral("protocol"), pod.protocol); addParam<>(jo, QStringLiteral("fields"), pod.fields); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, ThirdPartyLocation& result) { fromJson(jo.value("alias"_ls), result.alias); fromJson(jo.value("protocol"_ls), result.protocol); fromJson(jo.value("fields"_ls), result.fields); } spectral/include/libQuotient/lib/application-service/definitions/protocol.cpp0000644000175000000620000000447513566674122027707 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "protocol.h" using namespace Quotient; void JsonObjectConverter::dumpTo(QJsonObject& jo, const FieldType& pod) { addParam<>(jo, QStringLiteral("regexp"), pod.regexp); addParam<>(jo, QStringLiteral("placeholder"), pod.placeholder); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, FieldType& result) { fromJson(jo.value("regexp"_ls), result.regexp); fromJson(jo.value("placeholder"_ls), result.placeholder); } void JsonObjectConverter::dumpTo(QJsonObject& jo, const ProtocolInstance& pod) { addParam<>(jo, QStringLiteral("desc"), pod.desc); addParam(jo, QStringLiteral("icon"), pod.icon); addParam<>(jo, QStringLiteral("fields"), pod.fields); addParam<>(jo, QStringLiteral("network_id"), pod.networkId); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, ProtocolInstance& result) { fromJson(jo.value("desc"_ls), result.desc); fromJson(jo.value("icon"_ls), result.icon); fromJson(jo.value("fields"_ls), result.fields); fromJson(jo.value("network_id"_ls), result.networkId); } void JsonObjectConverter::dumpTo( QJsonObject& jo, const ThirdPartyProtocol& pod) { addParam<>(jo, QStringLiteral("user_fields"), pod.userFields); addParam<>(jo, QStringLiteral("location_fields"), pod.locationFields); addParam<>(jo, QStringLiteral("icon"), pod.icon); addParam<>(jo, QStringLiteral("field_types"), pod.fieldTypes); addParam<>(jo, QStringLiteral("instances"), pod.instances); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, ThirdPartyProtocol& result) { fromJson(jo.value("user_fields"_ls), result.userFields); fromJson(jo.value("location_fields"_ls), result.locationFields); fromJson(jo.value("icon"_ls), result.icon); fromJson(jo.value("field_types"_ls), result.fieldTypes); fromJson(jo.value("instances"_ls), result.instances); } spectral/include/libQuotient/lib/application-service/definitions/user.cpp0000644000175000000620000000147313566674122027017 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "user.h" using namespace Quotient; void JsonObjectConverter::dumpTo(QJsonObject& jo, const ThirdPartyUser& pod) { addParam<>(jo, QStringLiteral("userid"), pod.userid); addParam<>(jo, QStringLiteral("protocol"), pod.protocol); addParam<>(jo, QStringLiteral("fields"), pod.fields); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, ThirdPartyUser& result) { fromJson(jo.value("userid"_ls), result.userid); fromJson(jo.value("protocol"_ls), result.protocol); fromJson(jo.value("fields"_ls), result.fields); } spectral/include/libQuotient/lib/application-service/definitions/user.h0000644000175000000620000000137013566674122026460 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include namespace Quotient { // Data structures struct ThirdPartyUser { /// A Matrix User ID represting a third party user. QString userid; /// The protocol ID that the third party location is a part of. QString protocol; /// Information used to identify this third party location. QJsonObject fields; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const ThirdPartyUser& pod); static void fillFrom(const QJsonObject& jo, ThirdPartyUser& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/application-service/definitions/protocol.h0000644000175000000620000000601213566674122027341 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include #include #include namespace Quotient { // Data structures /// Definition of valid values for a field. struct FieldType { /// A regular expression for validation of a field's value. This may be /// relativelycoarse to verify the value as the application service /// providing this protocolmay apply additional validation or filtering. QString regexp; /// An placeholder serving as a valid example of the field value. QString placeholder; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const FieldType& pod); static void fillFrom(const QJsonObject& jo, FieldType& pod); }; struct ProtocolInstance { /// A human-readable description for the protocol, such as the name. QString desc; /// An optional content URI representing the protocol. Overrides the one /// providedat the higher level Protocol object. QString icon; /// Preset values for ``fields`` the client may use to search by. QJsonObject fields; /// A unique identifier across all instances. QString networkId; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const ProtocolInstance& pod); static void fillFrom(const QJsonObject& jo, ProtocolInstance& pod); }; struct ThirdPartyProtocol { /// Fields which may be used to identify a third party user. These should /// beordered to suggest the way that entities may be grouped, where /// highergroupings are ordered first. For example, the name of a network /// should besearched before the nickname of a user. QStringList userFields; /// Fields which may be used to identify a third party location. These /// should beordered to suggest the way that entities may be grouped, where /// highergroupings are ordered first. For example, the name of a network /// should besearched before the name of a channel. QStringList locationFields; /// A content URI representing an icon for the third party protocol. QString icon; /// The type definitions for the fields defined in the ``user_fields`` and /// ``location_fields``. Each entry in those arrays MUST have an entry here. /// The``string`` key for this object is field name itself.May be an empty /// object if no fields are defined. QHash fieldTypes; /// A list of objects representing independent instances of /// configuration.For example, multiple networks on IRC if multiple are /// provided by thesame application service. QVector instances; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const ThirdPartyProtocol& pod); static void fillFrom(const QJsonObject& jo, ThirdPartyProtocol& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/application-service/definitions/location.h0000644000175000000620000000136313566674122027314 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include namespace Quotient { // Data structures struct ThirdPartyLocation { /// An alias for a matrix room. QString alias; /// The protocol ID that the third party location is a part of. QString protocol; /// Information used to identify this third party location. QJsonObject fields; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const ThirdPartyLocation& pod); static void fillFrom(const QJsonObject& jo, ThirdPartyLocation& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/syncdata.h0000644000175000000620000000737113566674122021043 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "joinstate.h" #include "events/stateevent.h" namespace Quotient { /// Room summary, as defined in MSC688 /** * Every member of this structure is an Omittable; as per the MSC, only * changed values are sent from the server so if nothing is in the payload * the respective member will be omitted. In particular, `heroes.omitted()` * means that nothing has come from the server; heroes.value().isEmpty() * means a peculiar case of a room with the only member - the current user. */ struct RoomSummary { Omittable joinedMemberCount; Omittable invitedMemberCount; Omittable heroes; //< mxids of users to take part in the room // name bool isEmpty() const; /// Merge the contents of another RoomSummary object into this one /// \return true, if the current object has changed; false otherwise bool merge(const RoomSummary& other); friend QDebug operator<<(QDebug dbg, const RoomSummary& rs); }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const RoomSummary& rs); static void fillFrom(const QJsonObject& jo, RoomSummary& rs); }; class SyncRoomData { public: QString roomId; JoinState joinState; RoomSummary summary; StateEvents state; RoomEvents timeline; Events ephemeral; Events accountData; bool timelineLimited; QString timelinePrevBatch; int unreadCount; int highlightCount; int notificationCount; SyncRoomData(const QString& roomId, JoinState joinState_, const QJsonObject& room_); SyncRoomData(SyncRoomData&&) = default; SyncRoomData& operator=(SyncRoomData&&) = default; static const QString UnreadCountKey; }; // QVector cannot work with non-copiable objects, std::vector can. using SyncDataList = std::vector; class SyncData { public: SyncData() = default; explicit SyncData(const QString& cacheFileName); /** Parse sync response into room events * \param json response from /sync or a room state cache * \return the list of rooms with missing cache files; always * empty when parsing response from /sync */ void parseJson(const QJsonObject& json, const QString& baseDir = {}); Events&& takePresenceData(); Events&& takeAccountData(); Events&& takeToDeviceEvents(); SyncDataList&& takeRoomData(); QString nextBatch() const { return nextBatch_; } QStringList unresolvedRooms() const { return unresolvedRoomIds; } static std::pair cacheVersion() { return { 10, 0 }; } static QString fileNameForRoom(QString roomId); private: QString nextBatch_; Events presenceData; Events accountData; Events toDeviceEvents; SyncDataList roomData; QStringList unresolvedRoomIds; static QJsonObject loadJson(const QString& fileName); }; } // namespace Quotient spectral/include/libQuotient/lib/encryptionmanager.cpp0000644000175000000620000001771013566674122023313 0ustar dilingerstaff#include "encryptionmanager.h" #include "connection.h" #include "e2ee.h" #include "csapi/keys.h" #include #include #include // QtOlm #include #include using namespace Quotient; using namespace QtOlm; using std::move; class EncryptionManager::Private { public: explicit Private(const QByteArray& encryptionAccountPickle, float signedKeysProportion, float oneTimeKeyThreshold) : signedKeysProportion(move(signedKeysProportion)) , oneTimeKeyThreshold(move(oneTimeKeyThreshold)) { Q_ASSERT((0 <= signedKeysProportion) && (signedKeysProportion <= 1)); Q_ASSERT((0 <= oneTimeKeyThreshold) && (oneTimeKeyThreshold <= 1)); if (encryptionAccountPickle.isEmpty()) { olmAccount.reset(new Account()); } else { olmAccount.reset( new Account(encryptionAccountPickle)); // TODO: passphrase even // with qtkeychain? } /* * Note about targetKeysNumber: * * From: https://github.com/Zil0/matrix-python-sdk/ * File: matrix_client/crypto/olm_device.py * * Try to maintain half the number of one-time keys libolm can hold * uploaded on the HS. This is because some keys will be claimed by * peers but not used instantly, and we want them to stay in libolm, * until the limit is reached and it starts discarding keys, starting by * the oldest. */ targetKeysNumber = olmAccount->maxOneTimeKeys(); // 2 // see note below targetOneTimeKeyCounts = { { SignedCurve25519Key, qRound(signedKeysProportion * targetKeysNumber) }, { Curve25519Key, qRound((1 - signedKeysProportion) * targetKeysNumber) } }; } ~Private() = default; UploadKeysJob* uploadIdentityKeysJob = nullptr; UploadKeysJob* uploadOneTimeKeysJob = nullptr; QScopedPointer olmAccount; float signedKeysProportion; float oneTimeKeyThreshold; int targetKeysNumber; void updateKeysToUpload(); bool oneTimeKeyShouldUpload(); QHash oneTimeKeyCounts; void setOneTimeKeyCounts(const QHash oneTimeKeyCountsNewValue) { oneTimeKeyCounts = oneTimeKeyCountsNewValue; updateKeysToUpload(); } QHash oneTimeKeysToUploadCounts; QHash targetOneTimeKeyCounts; }; EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle, float signedKeysProportion, float oneTimeKeyThreshold, QObject* parent) : QObject(parent) , d(std::make_unique(std::move(encryptionAccountPickle), std::move(signedKeysProportion), std::move(oneTimeKeyThreshold))) {} EncryptionManager::~EncryptionManager() = default; void EncryptionManager::uploadIdentityKeys(Connection* connection) { // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-keys-upload DeviceKeys deviceKeys { /* * The ID of the user the device belongs to. Must match the user ID used * when logging in. The ID of the device these keys belong to. Must * match the device ID used when logging in. The encryption algorithms * supported by this device. */ connection->userId(), connection->deviceId(), SupportedAlgorithms, /* * Public identity keys. The names of the properties should be in the * format :. The keys themselves should be encoded * as specified by the key algorithm. */ { { Curve25519Key + QStringLiteral(":") + connection->deviceId(), d->olmAccount->curve25519IdentityKey() }, { Ed25519Key + QStringLiteral(":") + connection->deviceId(), d->olmAccount->ed25519IdentityKey() } }, /* signatures should be provided after the unsigned deviceKeys generation */ {} }; QJsonObject deviceKeysJsonObject = toJson(deviceKeys); /* additionally removing signatures key, * since we could not initialize deviceKeys * without an empty signatures value: */ deviceKeysJsonObject.remove(QStringLiteral("signatures")); /* * Signatures for the device key object. * A map from user ID, to a map from : to the * signature. The signature is calculated using the process called Signing * JSON. */ deviceKeys.signatures = { { connection->userId(), { { Ed25519Key + QStringLiteral(":") + connection->deviceId(), d->olmAccount->sign(deviceKeysJsonObject) } } } }; connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] { d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); qDebug() << QString("Uploaded identity keys."); }); d->uploadIdentityKeysJob = connection->callApi(deviceKeys); } void EncryptionManager::uploadOneTimeKeys(Connection* connection, bool forceUpdate) { if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) { auto job = connection->callApi(); connect(job, &BaseJob::success, this, [job, this] { d->setOneTimeKeyCounts(job->oneTimeKeyCounts()); }); } int signedKeysToUploadCount = d->oneTimeKeysToUploadCounts.value(SignedCurve25519Key, 0); int unsignedKeysToUploadCount = d->oneTimeKeysToUploadCounts.value(Curve25519Key, 0); d->olmAccount->generateOneTimeKeys(signedKeysToUploadCount + unsignedKeysToUploadCount); QHash oneTimeKeys = {}; const auto& olmAccountCurve25519OneTimeKeys = d->olmAccount->curve25519OneTimeKeys(); int oneTimeKeysCounter = 0; for (auto it = olmAccountCurve25519OneTimeKeys.cbegin(); it != olmAccountCurve25519OneTimeKeys.cend(); ++it) { QString keyId = it.key(); QString keyType; QVariant key; if (oneTimeKeysCounter < signedKeysToUploadCount) { QJsonObject message { { QStringLiteral("key"), it.value().toString() } }; key = d->olmAccount->sign(message); keyType = SignedCurve25519Key; } else { key = it.value(); keyType = Curve25519Key; } ++oneTimeKeysCounter; oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key); } d->uploadOneTimeKeysJob = connection->callApi(none, oneTimeKeys); d->olmAccount->markKeysAsPublished(); qDebug() << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.") .arg(signedKeysToUploadCount) .arg(unsignedKeysToUploadCount); } QByteArray EncryptionManager::olmAccountPickle() { return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain? } QtOlm::Account* EncryptionManager::account() const { return d->olmAccount.data(); } void EncryptionManager::Private::updateKeysToUpload() { for (auto it = targetOneTimeKeyCounts.cbegin(); it != targetOneTimeKeyCounts.cend(); ++it) { int numKeys = oneTimeKeyCounts.value(it.key(), 0); int numToCreate = qMax(it.value() - numKeys, 0); oneTimeKeysToUploadCounts.insert(it.key(), numToCreate); } } bool EncryptionManager::Private::oneTimeKeyShouldUpload() { if (oneTimeKeyCounts.empty()) return true; for (auto it = targetOneTimeKeyCounts.cbegin(); it != targetOneTimeKeyCounts.cend(); ++it) { if (oneTimeKeyCounts.value(it.key(), 0) < it.value() * oneTimeKeyThreshold) return true; } return false; } spectral/include/libQuotient/lib/avatar.h0000644000175000000620000000367413566674122020515 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include #include #include #include namespace Quotient { class Connection; class Avatar { public: explicit Avatar(); explicit Avatar(QUrl url); Avatar(Avatar&&); ~Avatar(); Avatar& operator=(Avatar&&); using get_callback_t = std::function; using upload_callback_t = std::function; QImage get(Connection* connection, int dimension, get_callback_t callback) const; QImage get(Connection* connection, int w, int h, get_callback_t callback) const; bool upload(Connection* connection, const QString& fileName, upload_callback_t callback) const; bool upload(Connection* connection, QIODevice* source, upload_callback_t callback) const; QString mediaId() const; QUrl url() const; bool updateUrl(const QUrl& newUrl); private: class Private; std::unique_ptr d; }; } // namespace Quotient /// \deprecated Use namespace Quotient instead namespace QMatrixClient = Quotient;spectral/include/libQuotient/lib/e2ee.h0000644000175000000620000000236613566674122020054 0ustar dilingerstaff#pragma once #include "util.h" #include namespace Quotient { inline const auto CiphertextKeyL = "ciphertext"_ls; inline const auto SenderKeyKeyL = "sender_key"_ls; inline const auto DeviceIdKeyL = "device_id"_ls; inline const auto SessionIdKeyL = "session_id"_ls; inline const auto AlgorithmKeyL = "algorithm"_ls; inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; inline const auto AlgorithmKey = QStringLiteral("algorithm"); inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms"); inline const auto RotationPeriodMsgsKey = QStringLiteral("rotation_period_msgs"); inline const auto Ed25519Key = QStringLiteral("ed25519"); inline const auto Curve25519Key = QStringLiteral("curve25519"); inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519"); inline const auto OlmV1Curve25519AesSha2AlgoKey = QStringLiteral("m.olm.v1.curve25519-aes-sha2"); inline const auto MegolmV1AesSha2AlgoKey = QStringLiteral("m.megolm.v1.aes-sha2"); inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/0002755000175000000620000000000013566674122020155 5ustar dilingerstaffspectral/include/libQuotient/lib/csapi/message_pagination.cpp0000644000175000000620000000465213566674122024523 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "message_pagination.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class GetRoomEventsJob::Private { public: QString begin; QString end; RoomEvents chunk; }; BaseJob::Query queryToGetRoomEvents(const QString& from, const QString& to, const QString& dir, Omittable limit, const QString& filter) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("from"), from); addParam(_q, QStringLiteral("to"), to); addParam<>(_q, QStringLiteral("dir"), dir); addParam(_q, QStringLiteral("limit"), limit); addParam(_q, QStringLiteral("filter"), filter); return _q; } QUrl GetRoomEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& from, const QString& dir, const QString& to, Omittable limit, const QString& filter) { return BaseJob::makeRequestUrl( std::move(baseUrl), basePath % "/rooms/" % roomId % "/messages", queryToGetRoomEvents(from, to, dir, limit, filter)); } static const auto GetRoomEventsJobName = QStringLiteral("GetRoomEventsJob"); GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& from, const QString& dir, const QString& to, Omittable limit, const QString& filter) : BaseJob(HttpVerb::Get, GetRoomEventsJobName, basePath % "/rooms/" % roomId % "/messages", queryToGetRoomEvents(from, to, dir, limit, filter)) , d(new Private) {} GetRoomEventsJob::~GetRoomEventsJob() = default; const QString& GetRoomEventsJob::begin() const { return d->begin; } const QString& GetRoomEventsJob::end() const { return d->end; } RoomEvents&& GetRoomEventsJob::chunk() { return std::move(d->chunk); } BaseJob::Status GetRoomEventsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("start"_ls), d->begin); fromJson(json.value("end"_ls), d->end); fromJson(json.value("chunk"_ls), d->chunk); return Success; } spectral/include/libQuotient/lib/csapi/filter.cpp0000644000175000000620000000405513566674122022150 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "filter.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class DefineFilterJob::Private { public: QString filterId; }; static const auto DefineFilterJobName = QStringLiteral("DefineFilterJob"); DefineFilterJob::DefineFilterJob(const QString& userId, const Filter& filter) : BaseJob(HttpVerb::Post, DefineFilterJobName, basePath % "/user/" % userId % "/filter") , d(new Private) { setRequestData(Data(toJson(filter))); } DefineFilterJob::~DefineFilterJob() = default; const QString& DefineFilterJob::filterId() const { return d->filterId; } BaseJob::Status DefineFilterJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("filter_id"_ls)) return { IncorrectResponse, "The key 'filter_id' not found in the response" }; fromJson(json.value("filter_id"_ls), d->filterId); return Success; } class GetFilterJob::Private { public: Filter data; }; QUrl GetFilterJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& filterId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/user/" % userId % "/filter/" % filterId); } static const auto GetFilterJobName = QStringLiteral("GetFilterJob"); GetFilterJob::GetFilterJob(const QString& userId, const QString& filterId) : BaseJob(HttpVerb::Get, GetFilterJobName, basePath % "/user/" % userId % "/filter/" % filterId) , d(new Private) {} GetFilterJob::~GetFilterJob() = default; const Filter& GetFilterJob::data() const { return d->data; } BaseJob::Status GetFilterJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } spectral/include/libQuotient/lib/csapi/registration.h0000644000175000000620000004625513566674122023052 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/../identity/definitions/sid.h" #include "csapi/definitions/auth_data.h" #include "jobs/basejob.h" namespace Quotient { // Operations /// Register for an account on this homeserver. /*! * This API endpoint uses the `User-Interactive Authentication API`_. * * Register for an account on this homeserver. * * There are two kinds of user account: * * - `user` accounts. These accounts may use the full API described in this * specification. * * - `guest` accounts. These accounts may have limited permissions and may not * be supported by all servers. * * If registration is successful, this endpoint will issue an access token * the client can use to authorize itself in subsequent requests. * * If the client does not supply a ``device_id``, the server must * auto-generate one. * * The server SHOULD register an account with a User ID based on the * ``username`` provided, if any. Note that the grammar of Matrix User ID * localparts is restricted, so the server MUST either map the provided * ``username`` onto a ``user_id`` in a logical manner, or reject * ``username``\s which do not comply to the grammar, with * ``M_INVALID_USERNAME``. * * Matrix clients MUST NOT assume that localpart of the registered * ``user_id`` matches the provided ``username``. * * The returned access token must be associated with the ``device_id`` * supplied by the client or generated by the server. The server may * invalidate any access token previously associated with that device. See * `Relationship between access tokens and devices`_. */ class RegisterJob : public BaseJob { public: /*! Register for an account on this homeserver. * \param kind * The kind of account to register. Defaults to `user`. * \param auth * Additional authentication information for the * user-interactive authentication API. Note that this * information is *not* used to define how the registered user * should be authenticated, but is instead used to * authenticate the ``register`` call itself. It should be * left empty, or omitted, unless an earlier call returned an * response with status code 401. * \param bindEmail * If true, the server binds the email used for authentication to * the Matrix ID with the identity server. * \param username * The basis for the localpart of the desired Matrix ID. If omitted, * the homeserver MUST generate a Matrix ID local part. * \param password * The desired password for the account. * \param deviceId * ID of the client device. If this does not correspond to a * known client device, a new device will be created. The server * will auto-generate a device_id if this is not specified. * \param initialDeviceDisplayName * A display name to assign to the newly-created device. Ignored * if ``device_id`` corresponds to a known device. * \param inhibitLogin * If true, an ``access_token`` and ``device_id`` should not be * returned from this call, therefore preventing an automatic * login. Defaults to false. */ explicit RegisterJob(const QString& kind = QStringLiteral("user"), const Omittable& auth = none, Omittable bindEmail = none, const QString& username = {}, const QString& password = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, Omittable inhibitLogin = none); ~RegisterJob() override; // Result properties /// The fully-qualified Matrix user ID (MXID) that has been registered. /// /// Any user ID returned by this API must conform to the grammar given in /// the `Matrix specification /// `_. const QString& userId() const; /// An access token for the account. /// This access token can then be used to authorize other requests. /// Required if the ``inhibit_login`` option is false. const QString& accessToken() const; /// The server_name of the homeserver on which the account has /// been registered. /// /// **Deprecated**. Clients should extract the server_name from /// ``user_id`` (by splitting at the first colon) if they require /// it. Note also that ``homeserver`` is not spelt this way. const QString& homeServer() const; /// ID of the registered device. Will be the same as the /// corresponding parameter in the request, if one was specified. /// Required if the ``inhibit_login`` option is false. const QString& deviceId() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Begins the validation process for an email to be used during registration. /*! * Proxies the Identity Service API ``validate/email/requestToken``, but * first checks that the given email address is not already associated * with an account on this homeserver. See the Identity Service API for * further information. */ class RequestTokenToRegisterEmailJob : public BaseJob { public: /*! Begins the validation process for an email to be used during * registration. \param clientSecret A unique string generated by the * client, and used to identify the validation attempt. It must be a string * consisting of the characters * ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it * must not be empty. * \param email * The email address to validate. * \param sendAttempt * The server will only send an email if the ``send_attempt`` * is a number greater than the most recent one which it has seen, * scoped to that ``email`` + ``client_secret`` pair. This is to * avoid repeatedly sending the same email in the case of request * retries between the POSTing user and the identity server. * The client should increment this value if they desire a new * email (e.g. a reminder) to be sent. * \param idServer * The hostname of the identity server to communicate with. May * optionally include a port. * \param nextLink * Optional. When the validation is completed, the identity * server will redirect the user to this URL. */ explicit RequestTokenToRegisterEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink = {}); ~RequestTokenToRegisterEmailJob() override; // Result properties /// An email has been sent to the specified address. /// Note that this may be an email containing the validation token or it may /// be informing the user of an error. const Sid& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Requests a validation token be sent to the given phone number for the /// purpose of registering an account /*! * Proxies the Identity Service API ``validate/msisdn/requestToken``, but * first checks that the given phone number is not already associated * with an account on this homeserver. See the Identity Service API for * further information. */ class RequestTokenToRegisterMSISDNJob : public BaseJob { public: /*! Requests a validation token be sent to the given phone number for the * purpose of registering an account \param clientSecret A unique string * generated by the client, and used to identify the validation attempt. It * must be a string consisting of the characters * ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it * must not be empty. * \param country * The two-letter uppercase ISO country code that the number in * ``phone_number`` should be parsed as if it were dialled from. * \param phoneNumber * The phone number to validate. * \param sendAttempt * The server will only send an SMS if the ``send_attempt`` is a * number greater than the most recent one which it has seen, * scoped to that ``country`` + ``phone_number`` + ``client_secret`` * triple. This is to avoid repeatedly sending the same SMS in * the case of request retries between the POSTing user and the * identity server. The client should increment this value if * they desire a new SMS (e.g. a reminder) to be sent. * \param idServer * The hostname of the identity server to communicate with. May * optionally include a port. * \param nextLink * Optional. When the validation is completed, the identity * server will redirect the user to this URL. */ explicit RequestTokenToRegisterMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink = {}); ~RequestTokenToRegisterMSISDNJob() override; // Result properties /// An SMS message has been sent to the specified phone number. /// Note that this may be an SMS message containing the validation token or /// it may be informing the user of an error. const Sid& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Changes a user's password. /*! * Changes the password for an account on this homeserver. * * This API endpoint uses the `User-Interactive Authentication API`_. * * An access token should be submitted to this endpoint if the client has * an active session. * * The homeserver may change the flows available depending on whether a * valid access token is provided. */ class ChangePasswordJob : public BaseJob { public: /*! Changes a user's password. * \param newPassword * The new password for the account. * \param auth * Additional authentication information for the user-interactive * authentication API. */ explicit ChangePasswordJob(const QString& newPassword, const Omittable& auth = none); }; /// Requests a validation token be sent to the given email address for the /// purpose of resetting a user's password /*! * Proxies the Identity Service API ``validate/email/requestToken``, but * first checks that the given email address **is** associated with an account * on this homeserver. This API should be used to request * validation tokens when authenticating for the * `account/password` endpoint. This API's parameters and response are * identical to that of the HS API |/register/email/requestToken|_ except that * `M_THREEPID_NOT_FOUND` may be returned if no account matching the * given email address could be found. The server may instead send an * email to the given address prompting the user to create an account. * `M_THREEPID_IN_USE` may not be returned. * * .. |/register/email/requestToken| replace:: ``/register/email/requestToken`` * * .. _/register/email/requestToken: * #post-matrix-client-r0-register-email-requesttoken */ class RequestTokenToResetPasswordEmailJob : public BaseJob { public: /*! Requests a validation token be sent to the given email address for the * purpose of resetting a user's password \param clientSecret A unique * string generated by the client, and used to identify the validation * attempt. It must be a string consisting of the characters * ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it * must not be empty. * \param email * The email address to validate. * \param sendAttempt * The server will only send an email if the ``send_attempt`` * is a number greater than the most recent one which it has seen, * scoped to that ``email`` + ``client_secret`` pair. This is to * avoid repeatedly sending the same email in the case of request * retries between the POSTing user and the identity server. * The client should increment this value if they desire a new * email (e.g. a reminder) to be sent. * \param idServer * The hostname of the identity server to communicate with. May * optionally include a port. * \param nextLink * Optional. When the validation is completed, the identity * server will redirect the user to this URL. */ explicit RequestTokenToResetPasswordEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink = {}); ~RequestTokenToResetPasswordEmailJob() override; // Result properties /// An email was sent to the given address. const Sid& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Requests a validation token be sent to the given phone number for the /// purpose of resetting a user's password. /*! * Proxies the Identity Service API ``validate/msisdn/requestToken``, but * first checks that the given phone number **is** associated with an account * on this homeserver. This API should be used to request * validation tokens when authenticating for the * `account/password` endpoint. This API's parameters and response are * identical to that of the HS API |/register/msisdn/requestToken|_ except that * `M_THREEPID_NOT_FOUND` may be returned if no account matching the * given phone number could be found. The server may instead send an * SMS message to the given address prompting the user to create an account. * `M_THREEPID_IN_USE` may not be returned. * * .. |/register/msisdn/requestToken| replace:: ``/register/msisdn/requestToken`` * * .. _/register/msisdn/requestToken: * #post-matrix-client-r0-register-email-requesttoken */ class RequestTokenToResetPasswordMSISDNJob : public BaseJob { public: /*! Requests a validation token be sent to the given phone number for the * purpose of resetting a user's password. \param clientSecret A unique * string generated by the client, and used to identify the validation * attempt. It must be a string consisting of the characters * ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it * must not be empty. * \param country * The two-letter uppercase ISO country code that the number in * ``phone_number`` should be parsed as if it were dialled from. * \param phoneNumber * The phone number to validate. * \param sendAttempt * The server will only send an SMS if the ``send_attempt`` is a * number greater than the most recent one which it has seen, * scoped to that ``country`` + ``phone_number`` + ``client_secret`` * triple. This is to avoid repeatedly sending the same SMS in * the case of request retries between the POSTing user and the * identity server. The client should increment this value if * they desire a new SMS (e.g. a reminder) to be sent. * \param idServer * The hostname of the identity server to communicate with. May * optionally include a port. * \param nextLink * Optional. When the validation is completed, the identity * server will redirect the user to this URL. */ explicit RequestTokenToResetPasswordMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink = {}); ~RequestTokenToResetPasswordMSISDNJob() override; // Result properties /// An SMS message was sent to the given phone number. const Sid& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Deactivate a user's account. /*! * Deactivate the user's account, removing all ability for the user to * login again. * * This API endpoint uses the `User-Interactive Authentication API`_. * * An access token should be submitted to this endpoint if the client has * an active session. * * The homeserver may change the flows available depending on whether a * valid access token is provided. */ class DeactivateAccountJob : public BaseJob { public: /*! Deactivate a user's account. * \param auth * Additional authentication information for the user-interactive * authentication API. */ explicit DeactivateAccountJob( const Omittable& auth = none); }; /// Checks to see if a username is available on the server. /*! * Checks to see if a username is available, and valid, for the server. * * The server should check to ensure that, at the time of the request, the * username requested is available for use. This includes verifying that an * application service has not claimed the username and that the username * fits the server's desired requirements (for example, a server could dictate * that it does not permit usernames with underscores). * * Matrix clients may wish to use this API prior to attempting registration, * however the clients must also be aware that using this API does not normally * reserve the username. This can mean that the username becomes unavailable * between checking its availability and attempting to register it. */ class CheckUsernameAvailabilityJob : public BaseJob { public: /*! Checks to see if a username is available on the server. * \param username * The username to check the availability of. */ explicit CheckUsernameAvailabilityJob(const QString& username); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * CheckUsernameAvailabilityJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& username); ~CheckUsernameAvailabilityJob() override; // Result properties /// A flag to indicate that the username is available. This should always /// be ``true`` when the server replies with 200 OK. Omittable available() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/filter.h0000644000175000000620000000447313566674122021621 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/definitions/sync_filter.h" #include "jobs/basejob.h" namespace Quotient { // Operations /// Upload a new filter. /*! * Uploads a new filter definition to the homeserver. * Returns a filter ID that may be used in future requests to * restrict which events are returned to the client. */ class DefineFilterJob : public BaseJob { public: /*! Upload a new filter. * \param userId * The id of the user uploading the filter. The access token must be * authorized to make requests for this user id. \param filter Uploads a new * filter definition to the homeserver. Returns a filter ID that may be used * in future requests to restrict which events are returned to the client. */ explicit DefineFilterJob(const QString& userId, const Filter& filter); ~DefineFilterJob() override; // Result properties /// The ID of the filter that was created. Cannot start /// with a ``{`` as this character is used to determine /// if the filter provided is inline JSON or a previously /// declared filter by homeservers on some APIs. const QString& filterId() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Download a filter class GetFilterJob : public BaseJob { public: /*! Download a filter * \param userId * The user ID to download a filter for. * \param filterId * The filter ID to download. */ explicit GetFilterJob(const QString& userId, const QString& filterId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetFilterJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& filterId); ~GetFilterJob() override; // Result properties /// "The filter defintion" const Filter& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/device_management.h0000644000175000000620000000674713566674122023775 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/definitions/auth_data.h" #include "csapi/definitions/client_device.h" #include "jobs/basejob.h" #include namespace Quotient { // Operations /// List registered devices for the current user /*! * Gets information about all devices for the current user. */ class GetDevicesJob : public BaseJob { public: explicit GetDevicesJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetDevicesJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); ~GetDevicesJob() override; // Result properties /// A list of all registered devices for this user. const QVector& devices() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Get a single device /*! * Gets information on a single device, by device id. */ class GetDeviceJob : public BaseJob { public: /*! Get a single device * \param deviceId * The device to retrieve. */ explicit GetDeviceJob(const QString& deviceId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetDeviceJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& deviceId); ~GetDeviceJob() override; // Result properties /// Device information const Device& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Update a device /*! * Updates the metadata on the given device. */ class UpdateDeviceJob : public BaseJob { public: /*! Update a device * \param deviceId * The device to update. * \param displayName * The new display name for this device. If not given, the * display name is unchanged. */ explicit UpdateDeviceJob(const QString& deviceId, const QString& displayName = {}); }; /// Delete a device /*! * This API endpoint uses the `User-Interactive Authentication API`_. * * Deletes the given device, and invalidates any access token associated with it. */ class DeleteDeviceJob : public BaseJob { public: /*! Delete a device * \param deviceId * The device to delete. * \param auth * Additional authentication information for the * user-interactive authentication API. */ explicit DeleteDeviceJob(const QString& deviceId, const Omittable& auth = none); }; /// Bulk deletion of devices /*! * This API endpoint uses the `User-Interactive Authentication API`_. * * Deletes the given devices, and invalidates any access token associated with * them. */ class DeleteDevicesJob : public BaseJob { public: /*! Bulk deletion of devices * \param devices * The list of device IDs to delete. * \param auth * Additional authentication information for the * user-interactive authentication API. */ explicit DeleteDevicesJob(const QStringList& devices, const Omittable& auth = none); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/leaving.cpp0000644000175000000620000000226013566674122022304 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "leaving.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); QUrl LeaveRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/rooms/" % roomId % "/leave"); } static const auto LeaveRoomJobName = QStringLiteral("LeaveRoomJob"); LeaveRoomJob::LeaveRoomJob(const QString& roomId) : BaseJob(HttpVerb::Post, LeaveRoomJobName, basePath % "/rooms/" % roomId % "/leave") {} QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/rooms/" % roomId % "/forget"); } static const auto ForgetRoomJobName = QStringLiteral("ForgetRoomJob"); ForgetRoomJob::ForgetRoomJob(const QString& roomId) : BaseJob(HttpVerb::Post, ForgetRoomJobName, basePath % "/rooms/" % roomId % "/forget") {} spectral/include/libQuotient/lib/csapi/notifications.cpp0000644000175000000620000000557613566674122023545 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "notifications.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); // Converters namespace Quotient { template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, GetNotificationsJob::Notification& result) { fromJson(jo.value("actions"_ls), result.actions); fromJson(jo.value("event"_ls), result.event); fromJson(jo.value("profile_tag"_ls), result.profileTag); fromJson(jo.value("read"_ls), result.read); fromJson(jo.value("room_id"_ls), result.roomId); fromJson(jo.value("ts"_ls), result.ts); } }; } // namespace Quotient class GetNotificationsJob::Private { public: QString nextToken; std::vector notifications; }; BaseJob::Query queryToGetNotifications(const QString& from, Omittable limit, const QString& only) { BaseJob::Query _q; addParam(_q, QStringLiteral("from"), from); addParam(_q, QStringLiteral("limit"), limit); addParam(_q, QStringLiteral("only"), only); return _q; } QUrl GetNotificationsJob::makeRequestUrl(QUrl baseUrl, const QString& from, Omittable limit, const QString& only) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/notifications", queryToGetNotifications(from, limit, only)); } static const auto GetNotificationsJobName = QStringLiteral("GetNotificationsJob"); GetNotificationsJob::GetNotificationsJob(const QString& from, Omittable limit, const QString& only) : BaseJob(HttpVerb::Get, GetNotificationsJobName, basePath % "/notifications", queryToGetNotifications(from, limit, only)) , d(new Private) {} GetNotificationsJob::~GetNotificationsJob() = default; const QString& GetNotificationsJob::nextToken() const { return d->nextToken; } std::vector&& GetNotificationsJob::notifications() { return std::move(d->notifications); } BaseJob::Status GetNotificationsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("next_token"_ls), d->nextToken); if (!json.contains("notifications"_ls)) return { IncorrectResponse, "The key 'notifications' not found in the response" }; fromJson(json.value("notifications"_ls), d->notifications); return Success; } spectral/include/libQuotient/lib/csapi/capabilities.h0000644000175000000620000000441213566674122022756 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "jobs/basejob.h" #include #include namespace Quotient { // Operations /// Gets information about the server's capabilities. /*! * Gets information about the server's supported feature set * and other relevant capabilities. */ class GetCapabilitiesJob : public BaseJob { public: // Inner data structures /// Capability to indicate if the user can change their password. struct ChangePasswordCapability { /// True if the user can change their password, false otherwise. bool enabled; }; /// The room versions the server supports. struct RoomVersionsCapability { /// The default room version the server is using for new rooms. QString defaultVersion; /// A detailed description of the room versions the server supports. QHash available; }; /// The custom capabilities the server supports, using theJava package /// naming convention. struct Capabilities { /// Capability to indicate if the user can change their password. Omittable changePassword; /// The room versions the server supports. Omittable roomVersions; /// The custom capabilities the server supports, using theJava package /// naming convention. QHash additionalProperties; }; // Construction/destruction explicit GetCapabilitiesJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetCapabilitiesJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); ~GetCapabilitiesJob() override; // Result properties /// The custom capabilities the server supports, using the /// Java package naming convention. const Capabilities& capabilities() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/to_device.h0000644000175000000620000000226213566674122022267 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" #include #include namespace Quotient { // Operations /// Send an event to a given set of devices. /*! * This endpoint is used to send send-to-device events to a set of * client devices. */ class SendToDeviceJob : public BaseJob { public: /*! Send an event to a given set of devices. * \param eventType * The type of event to send. * \param txnId * The transaction ID for this event. Clients should generate an * ID unique across requests with the same access token; it will be * used by the server to ensure idempotency of requests. * \param messages * The messages to send. A map from user ID, to a map from * device ID to message body. The device ID may also be `*`, * meaning all known devices for the user. */ explicit SendToDeviceJob( const QString& eventType, const QString& txnId, const QHash>& messages = {}); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/third_party_lookup.cpp0000644000175000000620000001565013566674122024610 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "third_party_lookup.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class GetProtocolsJob::Private { public: QHash data; }; QUrl GetProtocolsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/thirdparty/protocols"); } static const auto GetProtocolsJobName = QStringLiteral("GetProtocolsJob"); GetProtocolsJob::GetProtocolsJob() : BaseJob(HttpVerb::Get, GetProtocolsJobName, basePath % "/thirdparty/protocols") , d(new Private) {} GetProtocolsJob::~GetProtocolsJob() = default; const QHash& GetProtocolsJob::data() const { return d->data; } BaseJob::Status GetProtocolsJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } class GetProtocolMetadataJob::Private { public: ThirdPartyProtocol data; }; QUrl GetProtocolMetadataJob::makeRequestUrl(QUrl baseUrl, const QString& protocol) { return BaseJob::makeRequestUrl( std::move(baseUrl), basePath % "/thirdparty/protocol/" % protocol); } static const auto GetProtocolMetadataJobName = QStringLiteral("GetProtocolMetadataJob"); GetProtocolMetadataJob::GetProtocolMetadataJob(const QString& protocol) : BaseJob(HttpVerb::Get, GetProtocolMetadataJobName, basePath % "/thirdparty/protocol/" % protocol) , d(new Private) {} GetProtocolMetadataJob::~GetProtocolMetadataJob() = default; const ThirdPartyProtocol& GetProtocolMetadataJob::data() const { return d->data; } BaseJob::Status GetProtocolMetadataJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } class QueryLocationByProtocolJob::Private { public: QVector data; }; BaseJob::Query queryToQueryLocationByProtocol(const QString& searchFields) { BaseJob::Query _q; addParam(_q, QStringLiteral("searchFields"), searchFields); return _q; } QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& protocol, const QString& searchFields) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/thirdparty/location/" % protocol, queryToQueryLocationByProtocol(searchFields)); } static const auto QueryLocationByProtocolJobName = QStringLiteral("QueryLocationByProtocolJob"); QueryLocationByProtocolJob::QueryLocationByProtocolJob( const QString& protocol, const QString& searchFields) : BaseJob(HttpVerb::Get, QueryLocationByProtocolJobName, basePath % "/thirdparty/location/" % protocol, queryToQueryLocationByProtocol(searchFields)) , d(new Private) {} QueryLocationByProtocolJob::~QueryLocationByProtocolJob() = default; const QVector& QueryLocationByProtocolJob::data() const { return d->data; } BaseJob::Status QueryLocationByProtocolJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } class QueryUserByProtocolJob::Private { public: QVector data; }; BaseJob::Query queryToQueryUserByProtocol(const QString& fields) { BaseJob::Query _q; addParam(_q, QStringLiteral("fields..."), fields); return _q; } QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& protocol, const QString& fields) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/thirdparty/user/" % protocol, queryToQueryUserByProtocol(fields)); } static const auto QueryUserByProtocolJobName = QStringLiteral("QueryUserByProtocolJob"); QueryUserByProtocolJob::QueryUserByProtocolJob(const QString& protocol, const QString& fields) : BaseJob(HttpVerb::Get, QueryUserByProtocolJobName, basePath % "/thirdparty/user/" % protocol, queryToQueryUserByProtocol(fields)) , d(new Private) {} QueryUserByProtocolJob::~QueryUserByProtocolJob() = default; const QVector& QueryUserByProtocolJob::data() const { return d->data; } BaseJob::Status QueryUserByProtocolJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } class QueryLocationByAliasJob::Private { public: QVector data; }; BaseJob::Query queryToQueryLocationByAlias(const QString& alias) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("alias"), alias); return _q; } QUrl QueryLocationByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& alias) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/thirdparty/location", queryToQueryLocationByAlias(alias)); } static const auto QueryLocationByAliasJobName = QStringLiteral("QueryLocationByAliasJob"); QueryLocationByAliasJob::QueryLocationByAliasJob(const QString& alias) : BaseJob(HttpVerb::Get, QueryLocationByAliasJobName, basePath % "/thirdparty/location", queryToQueryLocationByAlias(alias)) , d(new Private) {} QueryLocationByAliasJob::~QueryLocationByAliasJob() = default; const QVector& QueryLocationByAliasJob::data() const { return d->data; } BaseJob::Status QueryLocationByAliasJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } class QueryUserByIDJob::Private { public: QVector data; }; BaseJob::Query queryToQueryUserByID(const QString& userid) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("userid"), userid); return _q; } QUrl QueryUserByIDJob::makeRequestUrl(QUrl baseUrl, const QString& userid) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/thirdparty/user", queryToQueryUserByID(userid)); } static const auto QueryUserByIDJobName = QStringLiteral("QueryUserByIDJob"); QueryUserByIDJob::QueryUserByIDJob(const QString& userid) : BaseJob(HttpVerb::Get, QueryUserByIDJobName, basePath % "/thirdparty/user", queryToQueryUserByID(userid)) , d(new Private) {} QueryUserByIDJob::~QueryUserByIDJob() = default; const QVector& QueryUserByIDJob::data() const { return d->data; } BaseJob::Status QueryUserByIDJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } spectral/include/libQuotient/lib/csapi/whoami.h0000644000175000000620000000243013566674122021607 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Gets information about the owner of an access token. /*! * Gets information about the owner of a given access token. * * Note that, as with the rest of the Client-Server API, * Application Services may masquerade as users within their * namespace by giving a ``user_id`` query parameter. In this * situation, the server should verify that the given ``user_id`` * is registered by the appservice, and return it in the response * body. */ class GetTokenOwnerJob : public BaseJob { public: explicit GetTokenOwnerJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetTokenOwnerJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); ~GetTokenOwnerJob() override; // Result properties /// The user id that owns the access token. const QString& userId() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/joining.cpp0000644000175000000620000000724313566674122022322 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "joining.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); // Converters namespace Quotient { template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const JoinRoomByIdJob::ThirdPartySigned& pod) { addParam<>(jo, QStringLiteral("sender"), pod.sender); addParam<>(jo, QStringLiteral("mxid"), pod.mxid); addParam<>(jo, QStringLiteral("token"), pod.token); addParam<>(jo, QStringLiteral("signatures"), pod.signatures); } }; } // namespace Quotient class JoinRoomByIdJob::Private { public: QString roomId; }; static const auto JoinRoomByIdJobName = QStringLiteral("JoinRoomByIdJob"); JoinRoomByIdJob::JoinRoomByIdJob( const QString& roomId, const Omittable& thirdPartySigned) : BaseJob(HttpVerb::Post, JoinRoomByIdJobName, basePath % "/rooms/" % roomId % "/join") , d(new Private) { QJsonObject _data; addParam(_data, QStringLiteral("third_party_signed"), thirdPartySigned); setRequestData(_data); } JoinRoomByIdJob::~JoinRoomByIdJob() = default; const QString& JoinRoomByIdJob::roomId() const { return d->roomId; } BaseJob::Status JoinRoomByIdJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("room_id"_ls)) return { IncorrectResponse, "The key 'room_id' not found in the response" }; fromJson(json.value("room_id"_ls), d->roomId); return Success; } // Converters namespace Quotient { template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const JoinRoomJob::Signed& pod) { addParam<>(jo, QStringLiteral("sender"), pod.sender); addParam<>(jo, QStringLiteral("mxid"), pod.mxid); addParam<>(jo, QStringLiteral("token"), pod.token); addParam<>(jo, QStringLiteral("signatures"), pod.signatures); } }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const JoinRoomJob::ThirdPartySigned& pod) { addParam<>(jo, QStringLiteral("signed"), pod.signedData); } }; } // namespace Quotient class JoinRoomJob::Private { public: QString roomId; }; BaseJob::Query queryToJoinRoom(const QStringList& serverName) { BaseJob::Query _q; addParam(_q, QStringLiteral("server_name"), serverName); return _q; } static const auto JoinRoomJobName = QStringLiteral("JoinRoomJob"); JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias, const QStringList& serverName, const Omittable& thirdPartySigned) : BaseJob(HttpVerb::Post, JoinRoomJobName, basePath % "/join/" % roomIdOrAlias, queryToJoinRoom(serverName)) , d(new Private) { QJsonObject _data; addParam(_data, QStringLiteral("third_party_signed"), thirdPartySigned); setRequestData(_data); } JoinRoomJob::~JoinRoomJob() = default; const QString& JoinRoomJob::roomId() const { return d->roomId; } BaseJob::Status JoinRoomJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("room_id"_ls)) return { IncorrectResponse, "The key 'room_id' not found in the response" }; fromJson(json.value("room_id"_ls), d->roomId); return Success; } spectral/include/libQuotient/lib/csapi/pushrules.cpp0000644000175000000620000002000713566674122022710 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "pushrules.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class GetPushRulesJob::Private { public: PushRuleset global; }; QUrl GetPushRulesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/pushrules"); } static const auto GetPushRulesJobName = QStringLiteral("GetPushRulesJob"); GetPushRulesJob::GetPushRulesJob() : BaseJob(HttpVerb::Get, GetPushRulesJobName, basePath % "/pushrules") , d(new Private) {} GetPushRulesJob::~GetPushRulesJob() = default; const PushRuleset& GetPushRulesJob::global() const { return d->global; } BaseJob::Status GetPushRulesJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("global"_ls)) return { IncorrectResponse, "The key 'global' not found in the response" }; fromJson(json.value("global"_ls), d->global); return Success; } class GetPushRuleJob::Private { public: PushRule data; }; QUrl GetPushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId); } static const auto GetPushRuleJobName = QStringLiteral("GetPushRuleJob"); GetPushRuleJob::GetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Get, GetPushRuleJobName, basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId) , d(new Private) {} GetPushRuleJob::~GetPushRuleJob() = default; const PushRule& GetPushRuleJob::data() const { return d->data; } BaseJob::Status GetPushRuleJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId); } static const auto DeletePushRuleJobName = QStringLiteral("DeletePushRuleJob"); DeletePushRuleJob::DeletePushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Delete, DeletePushRuleJobName, basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId) {} BaseJob::Query queryToSetPushRule(const QString& before, const QString& after) { BaseJob::Query _q; addParam(_q, QStringLiteral("before"), before); addParam(_q, QStringLiteral("after"), after); return _q; } static const auto SetPushRuleJobName = QStringLiteral("SetPushRuleJob"); SetPushRuleJob::SetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId, const QStringList& actions, const QString& before, const QString& after, const QVector& conditions, const QString& pattern) : BaseJob(HttpVerb::Put, SetPushRuleJobName, basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId, queryToSetPushRule(before, after)) { QJsonObject _data; addParam<>(_data, QStringLiteral("actions"), actions); addParam(_data, QStringLiteral("conditions"), conditions); addParam(_data, QStringLiteral("pattern"), pattern); setRequestData(_data); } class IsPushRuleEnabledJob::Private { public: bool enabled; }; QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/enabled"); } static const auto IsPushRuleEnabledJobName = QStringLiteral("IsPushRuleEnabledJob"); IsPushRuleEnabledJob::IsPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Get, IsPushRuleEnabledJobName, basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/enabled") , d(new Private) {} IsPushRuleEnabledJob::~IsPushRuleEnabledJob() = default; bool IsPushRuleEnabledJob::enabled() const { return d->enabled; } BaseJob::Status IsPushRuleEnabledJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("enabled"_ls)) return { IncorrectResponse, "The key 'enabled' not found in the response" }; fromJson(json.value("enabled"_ls), d->enabled); return Success; } static const auto SetPushRuleEnabledJobName = QStringLiteral("SetPushRuleEnabledJob"); SetPushRuleEnabledJob::SetPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId, bool enabled) : BaseJob(HttpVerb::Put, SetPushRuleEnabledJobName, basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/enabled") { QJsonObject _data; addParam<>(_data, QStringLiteral("enabled"), enabled); setRequestData(_data); } class GetPushRuleActionsJob::Private { public: QStringList actions; }; QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/actions"); } static const auto GetPushRuleActionsJobName = QStringLiteral("GetPushRuleActionsJob"); GetPushRuleActionsJob::GetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Get, GetPushRuleActionsJobName, basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/actions") , d(new Private) {} GetPushRuleActionsJob::~GetPushRuleActionsJob() = default; const QStringList& GetPushRuleActionsJob::actions() const { return d->actions; } BaseJob::Status GetPushRuleActionsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("actions"_ls)) return { IncorrectResponse, "The key 'actions' not found in the response" }; fromJson(json.value("actions"_ls), d->actions); return Success; } static const auto SetPushRuleActionsJobName = QStringLiteral("SetPushRuleActionsJob"); SetPushRuleActionsJob::SetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId, const QStringList& actions) : BaseJob(HttpVerb::Put, SetPushRuleActionsJobName, basePath % "/pushrules/" % scope % "/" % kind % "/" % ruleId % "/actions") { QJsonObject _data; addParam<>(_data, QStringLiteral("actions"), actions); setRequestData(_data); } spectral/include/libQuotient/lib/csapi/voip.cpp0000644000175000000620000000200613566674122021632 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "voip.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class GetTurnServerJob::Private { public: QJsonObject data; }; QUrl GetTurnServerJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/voip/turnServer"); } static const auto GetTurnServerJobName = QStringLiteral("GetTurnServerJob"); GetTurnServerJob::GetTurnServerJob() : BaseJob(HttpVerb::Get, GetTurnServerJobName, basePath % "/voip/turnServer") , d(new Private) {} GetTurnServerJob::~GetTurnServerJob() = default; const QJsonObject& GetTurnServerJob::data() const { return d->data; } BaseJob::Status GetTurnServerJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } spectral/include/libQuotient/lib/csapi/openid.cpp0000644000175000000620000000434313566674122022141 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "openid.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class RequestOpenIdTokenJob::Private { public: QString accessToken; QString tokenType; QString matrixServerName; int expiresIn; }; static const auto RequestOpenIdTokenJobName = QStringLiteral("RequestOpenIdTokenJob"); RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId, const QJsonObject& body) : BaseJob(HttpVerb::Post, RequestOpenIdTokenJobName, basePath % "/user/" % userId % "/openid/request_token") , d(new Private) { setRequestData(Data(toJson(body))); } RequestOpenIdTokenJob::~RequestOpenIdTokenJob() = default; const QString& RequestOpenIdTokenJob::accessToken() const { return d->accessToken; } const QString& RequestOpenIdTokenJob::tokenType() const { return d->tokenType; } const QString& RequestOpenIdTokenJob::matrixServerName() const { return d->matrixServerName; } int RequestOpenIdTokenJob::expiresIn() const { return d->expiresIn; } BaseJob::Status RequestOpenIdTokenJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("access_token"_ls)) return { IncorrectResponse, "The key 'access_token' not found in the response" }; fromJson(json.value("access_token"_ls), d->accessToken); if (!json.contains("token_type"_ls)) return { IncorrectResponse, "The key 'token_type' not found in the response" }; fromJson(json.value("token_type"_ls), d->tokenType); if (!json.contains("matrix_server_name"_ls)) return { IncorrectResponse, "The key 'matrix_server_name' not found in the response" }; fromJson(json.value("matrix_server_name"_ls), d->matrixServerName); if (!json.contains("expires_in"_ls)) return { IncorrectResponse, "The key 'expires_in' not found in the response" }; fromJson(json.value("expires_in"_ls), d->expiresIn); return Success; } spectral/include/libQuotient/lib/csapi/kicking.h0000644000175000000620000000233213566674122021743 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Kick a user from the room. /*! * Kick a user from the room. * * The caller must have the required power level in order to perform this * operation. * * Kicking a user adjusts the target member's membership state to be ``leave`` * with an optional ``reason``. Like with other membership changes, a user can * directly adjust the target member's state by making a request to * ``/rooms//state/m.room.member/``. */ class KickJob : public BaseJob { public: /*! Kick a user from the room. * \param roomId * The room identifier (not alias) from which the user should be kicked. * \param userId * The fully qualified user ID of the user being kicked. * \param reason * The reason the user has been kicked. This will be supplied as the * ``reason`` on the target's updated `m.room.member`_ event. */ explicit KickJob(const QString& roomId, const QString& userId, const QString& reason = {}); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/administrative_contact.cpp0000644000175000000620000001275213566674122025424 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "administrative_contact.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); // Converters namespace Quotient { template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, GetAccount3PIDsJob::ThirdPartyIdentifier& result) { fromJson(jo.value("medium"_ls), result.medium); fromJson(jo.value("address"_ls), result.address); fromJson(jo.value("validated_at"_ls), result.validatedAt); fromJson(jo.value("added_at"_ls), result.addedAt); } }; } // namespace Quotient class GetAccount3PIDsJob::Private { public: QVector threepids; }; QUrl GetAccount3PIDsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/account/3pid"); } static const auto GetAccount3PIDsJobName = QStringLiteral("GetAccount3PIDsJob"); GetAccount3PIDsJob::GetAccount3PIDsJob() : BaseJob(HttpVerb::Get, GetAccount3PIDsJobName, basePath % "/account/3pid") , d(new Private) {} GetAccount3PIDsJob::~GetAccount3PIDsJob() = default; const QVector& GetAccount3PIDsJob::threepids() const { return d->threepids; } BaseJob::Status GetAccount3PIDsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("threepids"_ls), d->threepids); return Success; } // Converters namespace Quotient { template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const Post3PIDsJob::ThreePidCredentials& pod) { addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); addParam<>(jo, QStringLiteral("id_server"), pod.idServer); addParam<>(jo, QStringLiteral("sid"), pod.sid); } }; } // namespace Quotient static const auto Post3PIDsJobName = QStringLiteral("Post3PIDsJob"); Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds, Omittable bind) : BaseJob(HttpVerb::Post, Post3PIDsJobName, basePath % "/account/3pid") { QJsonObject _data; addParam<>(_data, QStringLiteral("three_pid_creds"), threePidCreds); addParam(_data, QStringLiteral("bind"), bind); setRequestData(_data); } static const auto Delete3pidFromAccountJobName = QStringLiteral("Delete3pidFromAccountJob"); Delete3pidFromAccountJob::Delete3pidFromAccountJob(const QString& medium, const QString& address) : BaseJob(HttpVerb::Post, Delete3pidFromAccountJobName, basePath % "/account/3pid/delete") { QJsonObject _data; addParam<>(_data, QStringLiteral("medium"), medium); addParam<>(_data, QStringLiteral("address"), address); setRequestData(_data); } class RequestTokenTo3PIDEmailJob::Private { public: Sid data; }; static const auto RequestTokenTo3PIDEmailJobName = QStringLiteral("RequestTokenTo3PIDEmailJob"); RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob( const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink) : BaseJob(HttpVerb::Post, RequestTokenTo3PIDEmailJobName, basePath % "/account/3pid/email/requestToken", false) , d(new Private) { QJsonObject _data; addParam<>(_data, QStringLiteral("client_secret"), clientSecret); addParam<>(_data, QStringLiteral("email"), email); addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); addParam(_data, QStringLiteral("next_link"), nextLink); addParam<>(_data, QStringLiteral("id_server"), idServer); setRequestData(_data); } RequestTokenTo3PIDEmailJob::~RequestTokenTo3PIDEmailJob() = default; const Sid& RequestTokenTo3PIDEmailJob::data() const { return d->data; } BaseJob::Status RequestTokenTo3PIDEmailJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } class RequestTokenTo3PIDMSISDNJob::Private { public: Sid data; }; static const auto RequestTokenTo3PIDMSISDNJobName = QStringLiteral("RequestTokenTo3PIDMSISDNJob"); RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob( const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink) : BaseJob(HttpVerb::Post, RequestTokenTo3PIDMSISDNJobName, basePath % "/account/3pid/msisdn/requestToken", false) , d(new Private) { QJsonObject _data; addParam<>(_data, QStringLiteral("client_secret"), clientSecret); addParam<>(_data, QStringLiteral("country"), country); addParam<>(_data, QStringLiteral("phone_number"), phoneNumber); addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); addParam(_data, QStringLiteral("next_link"), nextLink); addParam<>(_data, QStringLiteral("id_server"), idServer); setRequestData(_data); } RequestTokenTo3PIDMSISDNJob::~RequestTokenTo3PIDMSISDNJob() = default; const Sid& RequestTokenTo3PIDMSISDNJob::data() const { return d->data; } BaseJob::Status RequestTokenTo3PIDMSISDNJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } spectral/include/libQuotient/lib/csapi/list_joined_rooms.cpp0000644000175000000620000000240713566674122024404 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "list_joined_rooms.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class GetJoinedRoomsJob::Private { public: QStringList joinedRooms; }; QUrl GetJoinedRoomsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/joined_rooms"); } static const auto GetJoinedRoomsJobName = QStringLiteral("GetJoinedRoomsJob"); GetJoinedRoomsJob::GetJoinedRoomsJob() : BaseJob(HttpVerb::Get, GetJoinedRoomsJobName, basePath % "/joined_rooms") , d(new Private) {} GetJoinedRoomsJob::~GetJoinedRoomsJob() = default; const QStringList& GetJoinedRoomsJob::joinedRooms() const { return d->joinedRooms; } BaseJob::Status GetJoinedRoomsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("joined_rooms"_ls)) return { IncorrectResponse, "The key 'joined_rooms' not found in the response" }; fromJson(json.value("joined_rooms"_ls), d->joinedRooms); return Success; } spectral/include/libQuotient/lib/csapi/profile.h0000644000175000000620000001075313566674122021772 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Set the user's display name. /*! * This API sets the given user's display name. You must have permission to * set this user's display name, e.g. you need to have their ``access_token``. */ class SetDisplayNameJob : public BaseJob { public: /*! Set the user's display name. * \param userId * The user whose display name to set. * \param displayname * The new display name for this user. */ explicit SetDisplayNameJob(const QString& userId, const QString& displayname = {}); }; /// Get the user's display name. /*! * Get the user's display name. This API may be used to fetch the user's * own displayname or to query the name of other users; either locally or * on remote homeservers. */ class GetDisplayNameJob : public BaseJob { public: /*! Get the user's display name. * \param userId * The user whose display name to get. */ explicit GetDisplayNameJob(const QString& userId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetDisplayNameJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); ~GetDisplayNameJob() override; // Result properties /// The user's display name if they have set one, otherwise not present. const QString& displayname() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Set the user's avatar URL. /*! * This API sets the given user's avatar URL. You must have permission to * set this user's avatar URL, e.g. you need to have their ``access_token``. */ class SetAvatarUrlJob : public BaseJob { public: /*! Set the user's avatar URL. * \param userId * The user whose avatar URL to set. * \param avatarUrl * The new avatar URL for this user. */ explicit SetAvatarUrlJob(const QString& userId, const QString& avatarUrl = {}); }; /// Get the user's avatar URL. /*! * Get the user's avatar URL. This API may be used to fetch the user's * own avatar URL or to query the URL of other users; either locally or * on remote homeservers. */ class GetAvatarUrlJob : public BaseJob { public: /*! Get the user's avatar URL. * \param userId * The user whose avatar URL to get. */ explicit GetAvatarUrlJob(const QString& userId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetAvatarUrlJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); ~GetAvatarUrlJob() override; // Result properties /// The user's avatar URL if they have set one, otherwise not present. const QString& avatarUrl() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Get this user's profile information. /*! * Get the combined profile information for this user. This API may be used * to fetch the user's own profile information or other users; either * locally or on remote homeservers. This API may return keys which are not * limited to ``displayname`` or ``avatar_url``. */ class GetUserProfileJob : public BaseJob { public: /*! Get this user's profile information. * \param userId * The user whose profile information to get. */ explicit GetUserProfileJob(const QString& userId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetUserProfileJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); ~GetUserProfileJob() override; // Result properties /// The user's avatar URL if they have set one, otherwise not present. const QString& avatarUrl() const; /// The user's display name if they have set one, otherwise not present. const QString& displayname() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/registration.cpp0000644000175000000620000002354613566674122023403 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "registration.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class RegisterJob::Private { public: QString userId; QString accessToken; QString homeServer; QString deviceId; }; BaseJob::Query queryToRegister(const QString& kind) { BaseJob::Query _q; addParam(_q, QStringLiteral("kind"), kind); return _q; } static const auto RegisterJobName = QStringLiteral("RegisterJob"); RegisterJob::RegisterJob(const QString& kind, const Omittable& auth, Omittable bindEmail, const QString& username, const QString& password, const QString& deviceId, const QString& initialDeviceDisplayName, Omittable inhibitLogin) : BaseJob(HttpVerb::Post, RegisterJobName, basePath % "/register", queryToRegister(kind), {}, false) , d(new Private) { QJsonObject _data; addParam(_data, QStringLiteral("auth"), auth); addParam(_data, QStringLiteral("bind_email"), bindEmail); addParam(_data, QStringLiteral("username"), username); addParam(_data, QStringLiteral("password"), password); addParam(_data, QStringLiteral("device_id"), deviceId); addParam(_data, QStringLiteral("initial_device_display_name"), initialDeviceDisplayName); addParam(_data, QStringLiteral("inhibit_login"), inhibitLogin); setRequestData(_data); } RegisterJob::~RegisterJob() = default; const QString& RegisterJob::userId() const { return d->userId; } const QString& RegisterJob::accessToken() const { return d->accessToken; } const QString& RegisterJob::homeServer() const { return d->homeServer; } const QString& RegisterJob::deviceId() const { return d->deviceId; } BaseJob::Status RegisterJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("user_id"_ls)) return { IncorrectResponse, "The key 'user_id' not found in the response" }; fromJson(json.value("user_id"_ls), d->userId); fromJson(json.value("access_token"_ls), d->accessToken); fromJson(json.value("home_server"_ls), d->homeServer); fromJson(json.value("device_id"_ls), d->deviceId); return Success; } class RequestTokenToRegisterEmailJob::Private { public: Sid data; }; static const auto RequestTokenToRegisterEmailJobName = QStringLiteral("RequestTokenToRegisterEmailJob"); RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob( const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink) : BaseJob(HttpVerb::Post, RequestTokenToRegisterEmailJobName, basePath % "/register/email/requestToken", false) , d(new Private) { QJsonObject _data; addParam<>(_data, QStringLiteral("client_secret"), clientSecret); addParam<>(_data, QStringLiteral("email"), email); addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); addParam(_data, QStringLiteral("next_link"), nextLink); addParam<>(_data, QStringLiteral("id_server"), idServer); setRequestData(_data); } RequestTokenToRegisterEmailJob::~RequestTokenToRegisterEmailJob() = default; const Sid& RequestTokenToRegisterEmailJob::data() const { return d->data; } BaseJob::Status RequestTokenToRegisterEmailJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } class RequestTokenToRegisterMSISDNJob::Private { public: Sid data; }; static const auto RequestTokenToRegisterMSISDNJobName = QStringLiteral("RequestTokenToRegisterMSISDNJob"); RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob( const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink) : BaseJob(HttpVerb::Post, RequestTokenToRegisterMSISDNJobName, basePath % "/register/msisdn/requestToken", false) , d(new Private) { QJsonObject _data; addParam<>(_data, QStringLiteral("client_secret"), clientSecret); addParam<>(_data, QStringLiteral("country"), country); addParam<>(_data, QStringLiteral("phone_number"), phoneNumber); addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); addParam(_data, QStringLiteral("next_link"), nextLink); addParam<>(_data, QStringLiteral("id_server"), idServer); setRequestData(_data); } RequestTokenToRegisterMSISDNJob::~RequestTokenToRegisterMSISDNJob() = default; const Sid& RequestTokenToRegisterMSISDNJob::data() const { return d->data; } BaseJob::Status RequestTokenToRegisterMSISDNJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } static const auto ChangePasswordJobName = QStringLiteral("ChangePasswordJob"); ChangePasswordJob::ChangePasswordJob(const QString& newPassword, const Omittable& auth) : BaseJob(HttpVerb::Post, ChangePasswordJobName, basePath % "/account/password") { QJsonObject _data; addParam<>(_data, QStringLiteral("new_password"), newPassword); addParam(_data, QStringLiteral("auth"), auth); setRequestData(_data); } class RequestTokenToResetPasswordEmailJob::Private { public: Sid data; }; static const auto RequestTokenToResetPasswordEmailJobName = QStringLiteral("RequestTokenToResetPasswordEmailJob"); RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob( const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink) : BaseJob(HttpVerb::Post, RequestTokenToResetPasswordEmailJobName, basePath % "/account/password/email/requestToken", false) , d(new Private) { QJsonObject _data; addParam<>(_data, QStringLiteral("client_secret"), clientSecret); addParam<>(_data, QStringLiteral("email"), email); addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); addParam(_data, QStringLiteral("next_link"), nextLink); addParam<>(_data, QStringLiteral("id_server"), idServer); setRequestData(_data); } RequestTokenToResetPasswordEmailJob::~RequestTokenToResetPasswordEmailJob() = default; const Sid& RequestTokenToResetPasswordEmailJob::data() const { return d->data; } BaseJob::Status RequestTokenToResetPasswordEmailJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } class RequestTokenToResetPasswordMSISDNJob::Private { public: Sid data; }; static const auto RequestTokenToResetPasswordMSISDNJobName = QStringLiteral("RequestTokenToResetPasswordMSISDNJob"); RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink) : BaseJob(HttpVerb::Post, RequestTokenToResetPasswordMSISDNJobName, basePath % "/account/password/msisdn/requestToken", false) , d(new Private) { QJsonObject _data; addParam<>(_data, QStringLiteral("client_secret"), clientSecret); addParam<>(_data, QStringLiteral("country"), country); addParam<>(_data, QStringLiteral("phone_number"), phoneNumber); addParam<>(_data, QStringLiteral("send_attempt"), sendAttempt); addParam(_data, QStringLiteral("next_link"), nextLink); addParam<>(_data, QStringLiteral("id_server"), idServer); setRequestData(_data); } RequestTokenToResetPasswordMSISDNJob::~RequestTokenToResetPasswordMSISDNJob() = default; const Sid& RequestTokenToResetPasswordMSISDNJob::data() const { return d->data; } BaseJob::Status RequestTokenToResetPasswordMSISDNJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } static const auto DeactivateAccountJobName = QStringLiteral("DeactivateAccountJob"); DeactivateAccountJob::DeactivateAccountJob( const Omittable& auth) : BaseJob(HttpVerb::Post, DeactivateAccountJobName, basePath % "/account/deactivate") { QJsonObject _data; addParam(_data, QStringLiteral("auth"), auth); setRequestData(_data); } class CheckUsernameAvailabilityJob::Private { public: Omittable available; }; BaseJob::Query queryToCheckUsernameAvailability(const QString& username) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("username"), username); return _q; } QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl, const QString& username) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/register/available", queryToCheckUsernameAvailability(username)); } static const auto CheckUsernameAvailabilityJobName = QStringLiteral("CheckUsernameAvailabilityJob"); CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& username) : BaseJob(HttpVerb::Get, CheckUsernameAvailabilityJobName, basePath % "/register/available", queryToCheckUsernameAvailability(username), {}, false) , d(new Private) {} CheckUsernameAvailabilityJob::~CheckUsernameAvailabilityJob() = default; Omittable CheckUsernameAvailabilityJob::available() const { return d->available; } BaseJob::Status CheckUsernameAvailabilityJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("available"_ls), d->available); return Success; } spectral/include/libQuotient/lib/csapi/preamble.mustache0000644000175000000620000000021613566674122023474 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ spectral/include/libQuotient/lib/csapi/room_state.h0000644000175000000620000001071713566674122022506 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Send a state event to the given room. /*! * State events can be sent using this endpoint. These events will be * overwritten if ````, ```` and ```` all * match. * * Requests to this endpoint **cannot use transaction IDs** * like other ``PUT`` paths because they cannot be differentiated from the * ``state_key``. Furthermore, ``POST`` is unsupported on state paths. * * The body of the request should be the content object of the event; the * fields in this object will vary depending on the type of event. See * `Room Events`_ for the ``m.`` event specification. */ class SetRoomStateWithKeyJob : public BaseJob { public: /*! Send a state event to the given room. * \param roomId * The room to set the state in * \param eventType * The type of event to send. * \param stateKey * The state_key for the state to send. Defaults to the empty string. * \param body * State events can be sent using this endpoint. These events will be * overwritten if ````, ```` and ```` all * match. * * Requests to this endpoint **cannot use transaction IDs** * like other ``PUT`` paths because they cannot be differentiated from the * ``state_key``. Furthermore, ``POST`` is unsupported on state paths. * * The body of the request should be the content object of the event; the * fields in this object will vary depending on the type of event. See * `Room Events`_ for the ``m.`` event specification. */ explicit SetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey, const QJsonObject& body = {}); ~SetRoomStateWithKeyJob() override; // Result properties /// A unique identifier for the event. const QString& eventId() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Send a state event to the given room. /*! * State events can be sent using this endpoint. This endpoint is * equivalent to calling `/rooms/{roomId}/state/{eventType}/{stateKey}` * with an empty `stateKey`. Previous state events with matching * `` and ``, and empty ``, will be overwritten. * * Requests to this endpoint **cannot use transaction IDs** * like other ``PUT`` paths because they cannot be differentiated from the * ``state_key``. Furthermore, ``POST`` is unsupported on state paths. * * The body of the request should be the content object of the event; the * fields in this object will vary depending on the type of event. See * `Room Events`_ for the ``m.`` event specification. */ class SetRoomStateJob : public BaseJob { public: /*! Send a state event to the given room. * \param roomId * The room to set the state in * \param eventType * The type of event to send. * \param body * State events can be sent using this endpoint. This endpoint is * equivalent to calling `/rooms/{roomId}/state/{eventType}/{stateKey}` * with an empty `stateKey`. Previous state events with matching * `` and ``, and empty ``, will be * overwritten. * * Requests to this endpoint **cannot use transaction IDs** * like other ``PUT`` paths because they cannot be differentiated from the * ``state_key``. Furthermore, ``POST`` is unsupported on state paths. * * The body of the request should be the content object of the event; the * fields in this object will vary depending on the type of event. See * `Room Events`_ for the ``m.`` event specification. */ explicit SetRoomStateJob(const QString& roomId, const QString& eventType, const QJsonObject& body = {}); ~SetRoomStateJob() override; // Result properties /// A unique identifier for the event. const QString& eventId() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/admin.h0000644000175000000620000000542513566674122021422 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "jobs/basejob.h" #include #include namespace Quotient { // Operations /// Gets information about a particular user. /*! * Gets information about a particular user. * * This API may be restricted to only be called by the user being looked * up, or by a server admin. Server-local administrator privileges are not * specified in this document. */ class GetWhoIsJob : public BaseJob { public: // Inner data structures /// Gets information about a particular user.This API may be restricted to /// only be called by the user being lookedup, or by a server admin. /// Server-local administrator privileges are notspecified in this document. struct ConnectionInfo { /// Most recently seen IP address of the session. QString ip; /// Unix timestamp that the session was last active. Omittable lastSeen; /// User agent string last seen in the session. QString userAgent; }; /// Gets information about a particular user.This API may be restricted to /// only be called by the user being lookedup, or by a server admin. /// Server-local administrator privileges are notspecified in this document. struct SessionInfo { /// Information particular connections in the session. QVector connections; }; /// Gets information about a particular user.This API may be restricted to /// only be called by the user being lookedup, or by a server admin. /// Server-local administrator privileges are notspecified in this document. struct DeviceInfo { /// A user's sessions (i.e. what they did with an access token from one /// login). QVector sessions; }; // Construction/destruction /*! Gets information about a particular user. * \param userId * The user to look up. */ explicit GetWhoIsJob(const QString& userId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetWhoIsJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); ~GetWhoIsJob() override; // Result properties /// The Matrix user ID of the user. const QString& userId() const; /// Each key is an identitfier for one of the user's devices. const QHash& devices() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/inviting.h0000644000175000000620000000254713566674122022163 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Invite a user to participate in a particular room. /*! * .. _invite-by-user-id-endpoint: * * *Note that there are two forms of this API, which are documented separately. * This version of the API requires that the inviter knows the Matrix * identifier of the invitee. The other is documented in the* * `third party invites section`_. * * This API invites a user to participate in a particular room. * They do not start participating in the room until they actually join the * room. * * Only users currently in a particular room can invite other users to * join that room. * * If the user was invited to the room, the homeserver will append a * ``m.room.member`` event to the room. * * .. _third party invites section: `invite-by-third-party-id-endpoint`_ */ class InviteUserJob : public BaseJob { public: /*! Invite a user to participate in a particular room. * \param roomId * The room identifier (not alias) to which to invite the user. * \param userId * The fully qualified user ID of the invitee. */ explicit InviteUserJob(const QString& roomId, const QString& userId); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/typing.cpp0000644000175000000620000000146313566674122022175 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "typing.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); static const auto SetTypingJobName = QStringLiteral("SetTypingJob"); SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId, bool typing, Omittable timeout) : BaseJob(HttpVerb::Put, SetTypingJobName, basePath % "/rooms/" % roomId % "/typing/" % userId) { QJsonObject _data; addParam<>(_data, QStringLiteral("typing"), typing); addParam(_data, QStringLiteral("timeout"), timeout); setRequestData(_data); } spectral/include/libQuotient/lib/csapi/administrative_contact.h0000644000175000000620000002235713566674122025073 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/../identity/definitions/sid.h" #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Gets a list of a user's third party identifiers. /*! * Gets a list of the third party identifiers that the homeserver has * associated with the user's account. * * This is *not* the same as the list of third party identifiers bound to * the user's Matrix ID in identity servers. * * Identifiers in this list may be used by the homeserver as, for example, * identifiers that it will accept to reset the user's account password. */ class GetAccount3PIDsJob : public BaseJob { public: // Inner data structures /// Gets a list of the third party identifiers that the homeserver /// hasassociated with the user's account.This is *not* the same as the list /// of third party identifiers bound tothe user's Matrix ID in identity /// servers.Identifiers in this list may be used by the homeserver as, for /// example,identifiers that it will accept to reset the user's account /// password. struct ThirdPartyIdentifier { /// The medium of the third party identifier. QString medium; /// The third party identifier address. QString address; /// The timestamp, in milliseconds, when the identifier wasvalidated by /// the identity server. qint64 validatedAt; /// The timestamp, in milliseconds, when the homeserver associated the /// third party identifier with the user. qint64 addedAt; }; // Construction/destruction explicit GetAccount3PIDsJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetAccount3PIDsJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); ~GetAccount3PIDsJob() override; // Result properties /// Gets a list of the third party identifiers that the homeserver has /// associated with the user's account. /// /// This is *not* the same as the list of third party identifiers bound to /// the user's Matrix ID in identity servers. /// /// Identifiers in this list may be used by the homeserver as, for example, /// identifiers that it will accept to reset the user's account password. const QVector& threepids() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Adds contact information to the user's account. /*! * Adds contact information to the user's account. */ class Post3PIDsJob : public BaseJob { public: // Inner data structures /// The third party credentials to associate with the account. struct ThreePidCredentials { /// The client secret used in the session with the identity server. QString clientSecret; /// The identity server to use. QString idServer; /// The session identifier given by the identity server. QString sid; }; // Construction/destruction /*! Adds contact information to the user's account. * \param threePidCreds * The third party credentials to associate with the account. * \param bind * Whether the homeserver should also bind this third party * identifier to the account's Matrix ID with the passed identity * server. Default: ``false``. */ explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds, Omittable bind = none); }; /// Deletes a third party identifier from the user's account /*! * Removes a third party identifier from the user's account. This might not * cause an unbind of the identifier from the identity server. */ class Delete3pidFromAccountJob : public BaseJob { public: /*! Deletes a third party identifier from the user's account * \param medium * The medium of the third party identifier being removed. * \param address * The third party address being removed. */ explicit Delete3pidFromAccountJob(const QString& medium, const QString& address); }; /// Begins the validation process for an email address for association with the /// user's account. /*! * Proxies the Identity Service API ``validate/email/requestToken``, but * first checks that the given email address is **not** already associated * with an account on this homeserver. This API should be used to request * validation tokens when adding an email address to an account. This API's * parameters and response are identical to that of the * |/register/email/requestToken|_ endpoint. */ class RequestTokenTo3PIDEmailJob : public BaseJob { public: /*! Begins the validation process for an email address for association with * the user's account. \param clientSecret A unique string generated by the * client, and used to identify the validation attempt. It must be a string * consisting of the characters * ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it * must not be empty. * \param email * The email address to validate. * \param sendAttempt * The server will only send an email if the ``send_attempt`` * is a number greater than the most recent one which it has seen, * scoped to that ``email`` + ``client_secret`` pair. This is to * avoid repeatedly sending the same email in the case of request * retries between the POSTing user and the identity server. * The client should increment this value if they desire a new * email (e.g. a reminder) to be sent. * \param idServer * The hostname of the identity server to communicate with. May * optionally include a port. * \param nextLink * Optional. When the validation is completed, the identity * server will redirect the user to this URL. */ explicit RequestTokenTo3PIDEmailJob(const QString& clientSecret, const QString& email, int sendAttempt, const QString& idServer, const QString& nextLink = {}); ~RequestTokenTo3PIDEmailJob() override; // Result properties /// An email was sent to the given address. const Sid& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Begins the validation process for a phone number for association with the /// user's account. /*! * Proxies the Identity Service API ``validate/msisdn/requestToken``, but * first checks that the given phone number is **not** already associated * with an account on this homeserver. This API should be used to request * validation tokens when adding a phone number to an account. This API's * parameters and response are identical to that of the * |/register/msisdn/requestToken|_ endpoint. */ class RequestTokenTo3PIDMSISDNJob : public BaseJob { public: /*! Begins the validation process for a phone number for association with * the user's account. \param clientSecret A unique string generated by the * client, and used to identify the validation attempt. It must be a string * consisting of the characters * ``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters and it * must not be empty. * \param country * The two-letter uppercase ISO country code that the number in * ``phone_number`` should be parsed as if it were dialled from. * \param phoneNumber * The phone number to validate. * \param sendAttempt * The server will only send an SMS if the ``send_attempt`` is a * number greater than the most recent one which it has seen, * scoped to that ``country`` + ``phone_number`` + ``client_secret`` * triple. This is to avoid repeatedly sending the same SMS in * the case of request retries between the POSTing user and the * identity server. The client should increment this value if * they desire a new SMS (e.g. a reminder) to be sent. * \param idServer * The hostname of the identity server to communicate with. May * optionally include a port. * \param nextLink * Optional. When the validation is completed, the identity * server will redirect the user to this URL. */ explicit RequestTokenTo3PIDMSISDNJob(const QString& clientSecret, const QString& country, const QString& phoneNumber, int sendAttempt, const QString& idServer, const QString& nextLink = {}); ~RequestTokenTo3PIDMSISDNJob() override; // Result properties /// An SMS message was sent to the given phone number. const Sid& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/rooms.h0000644000175000000620000002061313566674122021465 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "events/eventloader.h" #include "events/roommemberevent.h" #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Get a single event by event ID. /*! * Get a single event based on ``roomId/eventId``. You must have permission to * retrieve this event e.g. by being a member in the room for this event. */ class GetOneRoomEventJob : public BaseJob { public: /*! Get a single event by event ID. * \param roomId * The ID of the room the event is in. * \param eventId * The event ID to get. */ explicit GetOneRoomEventJob(const QString& roomId, const QString& eventId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetOneRoomEventJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId); ~GetOneRoomEventJob() override; // Result properties /// The full event. EventPtr&& data(); protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Get the state identified by the type and key. /*! * Looks up the contents of a state event in a room. If the user is * joined to the room then the state is taken from the current * state of the room. If the user has left the room then the state is * taken from the state of the room when they left. */ class GetRoomStateWithKeyJob : public BaseJob { public: /*! Get the state identified by the type and key. * \param roomId * The room to look up the state in. * \param eventType * The type of state to look up. * \param stateKey * The key of the state to look up. */ explicit GetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetRoomStateWithKeyJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey); }; /// Get the state identified by the type, with the empty state key. /*! * Looks up the contents of a state event in a room. If the user is * joined to the room then the state is taken from the current * state of the room. If the user has left the room then the state is * taken from the state of the room when they left. * * This looks up the state event with the empty state key. */ class GetRoomStateByTypeJob : public BaseJob { public: /*! Get the state identified by the type, with the empty state key. * \param roomId * The room to look up the state in. * \param eventType * The type of state to look up. */ explicit GetRoomStateByTypeJob(const QString& roomId, const QString& eventType); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetRoomStateByTypeJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType); }; /// Get all state events in the current state of a room. /*! * Get the state events for the current state of a room. */ class GetRoomStateJob : public BaseJob { public: /*! Get all state events in the current state of a room. * \param roomId * The room to look up the state for. */ explicit GetRoomStateJob(const QString& roomId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetRoomStateJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); ~GetRoomStateJob() override; // Result properties /// If the user is a member of the room this will be the /// current state of the room as a list of events. If the user /// has left the room then this will be the state of the room /// when they left as a list of events. StateEvents&& data(); protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Get the m.room.member events for the room. /*! * Get the list of members for this room. */ class GetMembersByRoomJob : public BaseJob { public: /*! Get the m.room.member events for the room. * \param roomId * The room to get the member events for. * \param at * The token defining the timeline position as-of which to return * the list of members. This token can be obtained from a batch token * returned for each room by the sync API, or from * a ``start``/``end`` token returned by a ``/messages`` request. * \param membership * Only return users with the specified membership * \param notMembership * Only return users with membership state other than specified */ explicit GetMembersByRoomJob(const QString& roomId, const QString& at = {}, const QString& membership = {}, const QString& notMembership = {}); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetMembersByRoomJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& at = {}, const QString& membership = {}, const QString& notMembership = {}); ~GetMembersByRoomJob() override; // Result properties /// Get the list of members for this room. EventsArray&& chunk(); protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Gets the list of currently joined users and their profile data. /*! * This API returns a map of MXIDs to member info objects for members of the * room. The current user must be in the room for it to work, unless it is an * Application Service in which case any of the AS's users must be in the room. * This API is primarily for Application Services and should be faster to * respond than ``/members`` as it can be implemented more efficiently on the * server. */ class GetJoinedMembersByRoomJob : public BaseJob { public: // Inner data structures /// This API returns a map of MXIDs to member info objects for members of /// the room. The current user must be in the room for it to work, unless it /// is an Application Service in which case any of the AS's users must be in /// the room. This API is primarily for Application Services and should be /// faster to respond than ``/members`` as it can be implemented more /// efficiently on the server. struct RoomMember { /// The display name of the user this object is representing. QString displayName; /// The mxc avatar url of the user this object is representing. QString avatarUrl; }; // Construction/destruction /*! Gets the list of currently joined users and their profile data. * \param roomId * The room to get the members of. */ explicit GetJoinedMembersByRoomJob(const QString& roomId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetJoinedMembersByRoomJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); ~GetJoinedMembersByRoomJob() override; // Result properties /// A map from user ID to a RoomMember object. const QHash& joined() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/presence.cpp0000644000175000000620000000446013566674122022467 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "presence.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); static const auto SetPresenceJobName = QStringLiteral("SetPresenceJob"); SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence, const QString& statusMsg) : BaseJob(HttpVerb::Put, SetPresenceJobName, basePath % "/presence/" % userId % "/status") { QJsonObject _data; addParam<>(_data, QStringLiteral("presence"), presence); addParam(_data, QStringLiteral("status_msg"), statusMsg); setRequestData(_data); } class GetPresenceJob::Private { public: QString presence; Omittable lastActiveAgo; QString statusMsg; Omittable currentlyActive; }; QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/presence/" % userId % "/status"); } static const auto GetPresenceJobName = QStringLiteral("GetPresenceJob"); GetPresenceJob::GetPresenceJob(const QString& userId) : BaseJob(HttpVerb::Get, GetPresenceJobName, basePath % "/presence/" % userId % "/status") , d(new Private) {} GetPresenceJob::~GetPresenceJob() = default; const QString& GetPresenceJob::presence() const { return d->presence; } Omittable GetPresenceJob::lastActiveAgo() const { return d->lastActiveAgo; } const QString& GetPresenceJob::statusMsg() const { return d->statusMsg; } Omittable GetPresenceJob::currentlyActive() const { return d->currentlyActive; } BaseJob::Status GetPresenceJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("presence"_ls)) return { IncorrectResponse, "The key 'presence' not found in the response" }; fromJson(json.value("presence"_ls), d->presence); fromJson(json.value("last_active_ago"_ls), d->lastActiveAgo); fromJson(json.value("status_msg"_ls), d->statusMsg); fromJson(json.value("currently_active"_ls), d->currentlyActive); return Success; } spectral/include/libQuotient/lib/csapi/message_pagination.h0000644000175000000620000000532713566674122024170 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "events/eventloader.h" #include "jobs/basejob.h" namespace Quotient { // Operations /// Get a list of events for this room /*! * This API returns a list of message and state events for a room. It uses * pagination query parameters to paginate history in the room. */ class GetRoomEventsJob : public BaseJob { public: /*! Get a list of events for this room * \param roomId * The room to get events from. * \param from * The token to start returning events from. This token can be obtained * from a ``prev_batch`` token returned for each room by the sync API, * or from a ``start`` or ``end`` token returned by a previous request * to this endpoint. * \param dir * The direction to return events from. * \param to * The token to stop returning events at. This token can be obtained from * a ``prev_batch`` token returned for each room by the sync endpoint, * or from a ``start`` or ``end`` token returned by a previous request to * this endpoint. * \param limit * The maximum number of events to return. Default: 10. * \param filter * A JSON RoomEventFilter to filter returned events with. */ explicit GetRoomEventsJob(const QString& roomId, const QString& from, const QString& dir, const QString& to = {}, Omittable limit = none, const QString& filter = {}); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetRoomEventsJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& from, const QString& dir, const QString& to = {}, Omittable limit = none, const QString& filter = {}); ~GetRoomEventsJob() override; // Result properties /// The token the pagination starts from. If ``dir=b`` this will be /// the token supplied in ``from``. const QString& begin() const; /// The token the pagination ends at. If ``dir=b`` this token should /// be used again to request even earlier events. const QString& end() const; /// A list of room events. RoomEvents&& chunk(); protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/users.cpp0000644000175000000620000000417613566674122022030 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "users.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); // Converters namespace Quotient { template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, SearchUserDirectoryJob::User& result) { fromJson(jo.value("user_id"_ls), result.userId); fromJson(jo.value("display_name"_ls), result.displayName); fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } }; } // namespace Quotient class SearchUserDirectoryJob::Private { public: QVector results; bool limited; }; static const auto SearchUserDirectoryJobName = QStringLiteral("SearchUserDirectoryJob"); SearchUserDirectoryJob::SearchUserDirectoryJob(const QString& searchTerm, Omittable limit) : BaseJob(HttpVerb::Post, SearchUserDirectoryJobName, basePath % "/user_directory/search") , d(new Private) { QJsonObject _data; addParam<>(_data, QStringLiteral("search_term"), searchTerm); addParam(_data, QStringLiteral("limit"), limit); setRequestData(_data); } SearchUserDirectoryJob::~SearchUserDirectoryJob() = default; const QVector& SearchUserDirectoryJob::results() const { return d->results; } bool SearchUserDirectoryJob::limited() const { return d->limited; } BaseJob::Status SearchUserDirectoryJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("results"_ls)) return { IncorrectResponse, "The key 'results' not found in the response" }; fromJson(json.value("results"_ls), d->results); if (!json.contains("limited"_ls)) return { IncorrectResponse, "The key 'limited' not found in the response" }; fromJson(json.value("limited"_ls), d->limited); return Success; } spectral/include/libQuotient/lib/csapi/kicking.cpp0000644000175000000620000000136713566674122022305 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "kicking.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); static const auto KickJobName = QStringLiteral("KickJob"); KickJob::KickJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, KickJobName, basePath % "/rooms/" % roomId % "/kick") { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); addParam(_data, QStringLiteral("reason"), reason); setRequestData(_data); } spectral/include/libQuotient/lib/csapi/receipts.h0000644000175000000620000000204713566674122022145 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Send a receipt for the given event ID. /*! * This API updates the marker for the given receipt type to the event ID * specified. */ class PostReceiptJob : public BaseJob { public: /*! Send a receipt for the given event ID. * \param roomId * The room in which to send the event. * \param receiptType * The type of receipt to send. * \param eventId * The event ID to acknowledge up to. * \param receipt * Extra receipt information to attach to ``content`` if any. The * server will automatically set the ``ts`` field. */ explicit PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, const QJsonObject& receipt = {}); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/tags.cpp0000644000175000000620000000547013566674122021623 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "tags.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); // Converters namespace Quotient { template <> struct JsonObjectConverter { static void fillFrom(QJsonObject jo, GetRoomTagsJob::Tag& result) { fromJson(jo.take("order"_ls), result.order); fromJson(jo, result.additionalProperties); } }; } // namespace Quotient class GetRoomTagsJob::Private { public: QHash tags; }; QUrl GetRoomTagsJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/user/" % userId % "/rooms/" % roomId % "/tags"); } static const auto GetRoomTagsJobName = QStringLiteral("GetRoomTagsJob"); GetRoomTagsJob::GetRoomTagsJob(const QString& userId, const QString& roomId) : BaseJob(HttpVerb::Get, GetRoomTagsJobName, basePath % "/user/" % userId % "/rooms/" % roomId % "/tags") , d(new Private) {} GetRoomTagsJob::~GetRoomTagsJob() = default; const QHash& GetRoomTagsJob::tags() const { return d->tags; } BaseJob::Status GetRoomTagsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("tags"_ls), d->tags); return Success; } static const auto SetRoomTagJobName = QStringLiteral("SetRoomTagJob"); SetRoomTagJob::SetRoomTagJob(const QString& userId, const QString& roomId, const QString& tag, Omittable order) : BaseJob(HttpVerb::Put, SetRoomTagJobName, basePath % "/user/" % userId % "/rooms/" % roomId % "/tags/" % tag) { QJsonObject _data; addParam(_data, QStringLiteral("order"), order); setRequestData(_data); } QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& tag) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/user/" % userId % "/rooms/" % roomId % "/tags/" % tag); } static const auto DeleteRoomTagJobName = QStringLiteral("DeleteRoomTagJob"); DeleteRoomTagJob::DeleteRoomTagJob(const QString& userId, const QString& roomId, const QString& tag) : BaseJob(HttpVerb::Delete, DeleteRoomTagJobName, basePath % "/user/" % userId % "/rooms/" % roomId % "/tags/" % tag) {} spectral/include/libQuotient/lib/csapi/third_party_membership.cpp0000644000175000000620000000161713566674122025430 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "third_party_membership.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); static const auto InviteBy3PIDJobName = QStringLiteral("InviteBy3PIDJob"); InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& medium, const QString& address) : BaseJob(HttpVerb::Post, InviteBy3PIDJobName, basePath % "/rooms/" % roomId % "/invite") { QJsonObject _data; addParam<>(_data, QStringLiteral("id_server"), idServer); addParam<>(_data, QStringLiteral("medium"), medium); addParam<>(_data, QStringLiteral("address"), address); setRequestData(_data); } spectral/include/libQuotient/lib/csapi/list_public_rooms.cpp0000644000175000000620000001244113566674122024411 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "list_public_rooms.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class GetRoomVisibilityOnDirectoryJob::Private { public: QString visibility; }; QUrl GetRoomVisibilityOnDirectoryJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/directory/list/room/" % roomId); } static const auto GetRoomVisibilityOnDirectoryJobName = QStringLiteral("GetRoomVisibilityOnDirectoryJob"); GetRoomVisibilityOnDirectoryJob::GetRoomVisibilityOnDirectoryJob( const QString& roomId) : BaseJob(HttpVerb::Get, GetRoomVisibilityOnDirectoryJobName, basePath % "/directory/list/room/" % roomId, false) , d(new Private) {} GetRoomVisibilityOnDirectoryJob::~GetRoomVisibilityOnDirectoryJob() = default; const QString& GetRoomVisibilityOnDirectoryJob::visibility() const { return d->visibility; } BaseJob::Status GetRoomVisibilityOnDirectoryJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("visibility"_ls), d->visibility); return Success; } static const auto SetRoomVisibilityOnDirectoryJobName = QStringLiteral("SetRoomVisibilityOnDirectoryJob"); SetRoomVisibilityOnDirectoryJob::SetRoomVisibilityOnDirectoryJob( const QString& roomId, const QString& visibility) : BaseJob(HttpVerb::Put, SetRoomVisibilityOnDirectoryJobName, basePath % "/directory/list/room/" % roomId) { QJsonObject _data; addParam(_data, QStringLiteral("visibility"), visibility); setRequestData(_data); } class GetPublicRoomsJob::Private { public: PublicRoomsResponse data; }; BaseJob::Query queryToGetPublicRooms(Omittable limit, const QString& since, const QString& server) { BaseJob::Query _q; addParam(_q, QStringLiteral("limit"), limit); addParam(_q, QStringLiteral("since"), since); addParam(_q, QStringLiteral("server"), server); return _q; } QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable limit, const QString& since, const QString& server) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/publicRooms", queryToGetPublicRooms(limit, since, server)); } static const auto GetPublicRoomsJobName = QStringLiteral("GetPublicRoomsJob"); GetPublicRoomsJob::GetPublicRoomsJob(Omittable limit, const QString& since, const QString& server) : BaseJob(HttpVerb::Get, GetPublicRoomsJobName, basePath % "/publicRooms", queryToGetPublicRooms(limit, since, server), {}, false) , d(new Private) {} GetPublicRoomsJob::~GetPublicRoomsJob() = default; const PublicRoomsResponse& GetPublicRoomsJob::data() const { return d->data; } BaseJob::Status GetPublicRoomsJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } // Converters namespace Quotient { template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const QueryPublicRoomsJob::Filter& pod) { addParam(jo, QStringLiteral("generic_search_term"), pod.genericSearchTerm); } }; } // namespace Quotient class QueryPublicRoomsJob::Private { public: PublicRoomsResponse data; }; BaseJob::Query queryToQueryPublicRooms(const QString& server) { BaseJob::Query _q; addParam(_q, QStringLiteral("server"), server); return _q; } static const auto QueryPublicRoomsJobName = QStringLiteral("QueryPublicRoomsJob"); QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable limit, const QString& since, const Omittable& filter, Omittable includeAllNetworks, const QString& thirdPartyInstanceId) : BaseJob(HttpVerb::Post, QueryPublicRoomsJobName, basePath % "/publicRooms", queryToQueryPublicRooms(server)) , d(new Private) { QJsonObject _data; addParam(_data, QStringLiteral("limit"), limit); addParam(_data, QStringLiteral("since"), since); addParam(_data, QStringLiteral("filter"), filter); addParam(_data, QStringLiteral("include_all_networks"), includeAllNetworks); addParam(_data, QStringLiteral("third_party_instance_id"), thirdPartyInstanceId); setRequestData(_data); } QueryPublicRoomsJob::~QueryPublicRoomsJob() = default; const PublicRoomsResponse& QueryPublicRoomsJob::data() const { return d->data; } BaseJob::Status QueryPublicRoomsJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } spectral/include/libQuotient/lib/csapi/pushrules.h0000644000175000000620000002123213566674122022356 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/definitions/push_condition.h" #include "csapi/definitions/push_rule.h" #include "csapi/definitions/push_ruleset.h" #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Retrieve all push rulesets. /*! * Retrieve all push rulesets for this user. Clients can "drill-down" on * the rulesets by suffixing a ``scope`` to this path e.g. * ``/pushrules/global/``. This will return a subset of this data under the * specified key e.g. the ``global`` key. */ class GetPushRulesJob : public BaseJob { public: explicit GetPushRulesJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetPushRulesJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); ~GetPushRulesJob() override; // Result properties /// The global ruleset. const PushRuleset& global() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Retrieve a push rule. /*! * Retrieve a single specified push rule. */ class GetPushRuleJob : public BaseJob { public: /*! Retrieve a push rule. * \param scope * ``global`` to specify global rules. * \param kind * The kind of rule * \param ruleId * The identifier for the rule. */ explicit GetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetPushRuleJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId); ~GetPushRuleJob() override; // Result properties /// The push rule. const PushRule& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Delete a push rule. /*! * This endpoint removes the push rule defined in the path. */ class DeletePushRuleJob : public BaseJob { public: /*! Delete a push rule. * \param scope * ``global`` to specify global rules. * \param kind * The kind of rule * \param ruleId * The identifier for the rule. */ explicit DeletePushRuleJob(const QString& scope, const QString& kind, const QString& ruleId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * DeletePushRuleJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId); }; /// Add or change a push rule. /*! * This endpoint allows the creation, modification and deletion of pushers * for this user ID. The behaviour of this endpoint varies depending on the * values in the JSON body. * * When creating push rules, they MUST be enabled by default. */ class SetPushRuleJob : public BaseJob { public: /*! Add or change a push rule. * \param scope * ``global`` to specify global rules. * \param kind * The kind of rule * \param ruleId * The identifier for the rule. * \param actions * The action(s) to perform when the conditions for this rule are met. * \param before * Use 'before' with a ``rule_id`` as its value to make the new rule the * next-most important rule with respect to the given user defined rule. * It is not possible to add a rule relative to a predefined server rule. * \param after * This makes the new rule the next-less important rule relative to the * given user defined rule. It is not possible to add a rule relative * to a predefined server rule. * \param conditions * The conditions that must hold true for an event in order for a * rule to be applied to an event. A rule with no conditions * always matches. Only applicable to ``underride`` and ``override`` * rules. \param pattern Only applicable to ``content`` rules. The * glob-style pattern to match against. */ explicit SetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId, const QStringList& actions, const QString& before = {}, const QString& after = {}, const QVector& conditions = {}, const QString& pattern = {}); }; /// Get whether a push rule is enabled /*! * This endpoint gets whether the specified push rule is enabled. */ class IsPushRuleEnabledJob : public BaseJob { public: /*! Get whether a push rule is enabled * \param scope * Either ``global`` or ``device/`` to specify global * rules or device rules for the given ``profile_tag``. * \param kind * The kind of rule * \param ruleId * The identifier for the rule. */ explicit IsPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * IsPushRuleEnabledJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId); ~IsPushRuleEnabledJob() override; // Result properties /// Whether the push rule is enabled or not. bool enabled() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Enable or disable a push rule. /*! * This endpoint allows clients to enable or disable the specified push rule. */ class SetPushRuleEnabledJob : public BaseJob { public: /*! Enable or disable a push rule. * \param scope * ``global`` to specify global rules. * \param kind * The kind of rule * \param ruleId * The identifier for the rule. * \param enabled * Whether the push rule is enabled or not. */ explicit SetPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId, bool enabled); }; /// The actions for a push rule /*! * This endpoint get the actions for the specified push rule. */ class GetPushRuleActionsJob : public BaseJob { public: /*! The actions for a push rule * \param scope * Either ``global`` or ``device/`` to specify global * rules or device rules for the given ``profile_tag``. * \param kind * The kind of rule * \param ruleId * The identifier for the rule. */ explicit GetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetPushRuleActionsJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId); ~GetPushRuleActionsJob() override; // Result properties /// The action(s) to perform for this rule. const QStringList& actions() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Set the actions for a push rule. /*! * This endpoint allows clients to change the actions of a push rule. * This can be used to change the actions of builtin rules. */ class SetPushRuleActionsJob : public BaseJob { public: /*! Set the actions for a push rule. * \param scope * ``global`` to specify global rules. * \param kind * The kind of rule * \param ruleId * The identifier for the rule. * \param actions * The action(s) to perform for this rule. */ explicit SetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId, const QStringList& actions); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/event_context.cpp0000644000175000000620000000500313566674122023542 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "event_context.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class GetEventContextJob::Private { public: QString begin; QString end; RoomEvents eventsBefore; RoomEventPtr event; RoomEvents eventsAfter; StateEvents state; }; BaseJob::Query queryToGetEventContext(Omittable limit) { BaseJob::Query _q; addParam(_q, QStringLiteral("limit"), limit); return _q; } QUrl GetEventContextJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId, Omittable limit) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/rooms/" % roomId % "/context/" % eventId, queryToGetEventContext(limit)); } static const auto GetEventContextJobName = QStringLiteral("GetEventContextJob"); GetEventContextJob::GetEventContextJob(const QString& roomId, const QString& eventId, Omittable limit) : BaseJob(HttpVerb::Get, GetEventContextJobName, basePath % "/rooms/" % roomId % "/context/" % eventId, queryToGetEventContext(limit)) , d(new Private) {} GetEventContextJob::~GetEventContextJob() = default; const QString& GetEventContextJob::begin() const { return d->begin; } const QString& GetEventContextJob::end() const { return d->end; } RoomEvents&& GetEventContextJob::eventsBefore() { return std::move(d->eventsBefore); } RoomEventPtr&& GetEventContextJob::event() { return std::move(d->event); } RoomEvents&& GetEventContextJob::eventsAfter() { return std::move(d->eventsAfter); } StateEvents&& GetEventContextJob::state() { return std::move(d->state); } BaseJob::Status GetEventContextJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("start"_ls), d->begin); fromJson(json.value("end"_ls), d->end); fromJson(json.value("events_before"_ls), d->eventsBefore); fromJson(json.value("event"_ls), d->event); fromJson(json.value("events_after"_ls), d->eventsAfter); fromJson(json.value("state"_ls), d->state); return Success; } spectral/include/libQuotient/lib/csapi/third_party_membership.h0000644000175000000620000000554413566674122025100 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Invite a user to participate in a particular room. /*! * .. _invite-by-third-party-id-endpoint: * * *Note that there are two forms of this API, which are documented separately. * This version of the API does not require that the inviter know the Matrix * identifier of the invitee, and instead relies on third party identifiers. * The homeserver uses an identity server to perform the mapping from * third party identifier to a Matrix identifier. The other is documented in * the* `joining rooms section`_. * * This API invites a user to participate in a particular room. * They do not start participating in the room until they actually join the * room. * * Only users currently in a particular room can invite other users to * join that room. * * If the identity server did know the Matrix user identifier for the * third party identifier, the homeserver will append a ``m.room.member`` * event to the room. * * If the identity server does not know a Matrix user identifier for the * passed third party identifier, the homeserver will issue an invitation * which can be accepted upon providing proof of ownership of the third * party identifier. This is achieved by the identity server generating a * token, which it gives to the inviting homeserver. The homeserver will * add an ``m.room.third_party_invite`` event into the graph for the room, * containing that token. * * When the invitee binds the invited third party identifier to a Matrix * user ID, the identity server will give the user a list of pending * invitations, each containing: * * - The room ID to which they were invited * * - The token given to the homeserver * * - A signature of the token, signed with the identity server's private key * * - The matrix user ID who invited them to the room * * If a token is requested from the identity server, the homeserver will * append a ``m.room.third_party_invite`` event to the room. * * .. _joining rooms section: `invite-by-user-id-endpoint`_ */ class InviteBy3PIDJob : public BaseJob { public: /*! Invite a user to participate in a particular room. * \param roomId * The room identifier (not alias) to which to invite the user. * \param idServer * The hostname+port of the identity server which should be used for third * party identifier lookups. \param medium The kind of address being passed * in the address field, for example ``email``. \param address The invitee's * third party identifier. */ explicit InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& medium, const QString& address); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/inviting.cpp0000644000175000000620000000125513566674122022511 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "inviting.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); static const auto InviteUserJobName = QStringLiteral("InviteUserJob"); InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId) : BaseJob(HttpVerb::Post, InviteUserJobName, basePath % "/rooms/" % roomId % "/invite") { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); setRequestData(_data); } spectral/include/libQuotient/lib/csapi/directory.cpp0000644000175000000620000000422513566674122022666 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "directory.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0/directory"); static const auto SetRoomAliasJobName = QStringLiteral("SetRoomAliasJob"); SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId) : BaseJob(HttpVerb::Put, SetRoomAliasJobName, basePath % "/room/" % roomAlias) { QJsonObject _data; addParam<>(_data, QStringLiteral("room_id"), roomId); setRequestData(_data); } class GetRoomIdByAliasJob::Private { public: QString roomId; QStringList servers; }; QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/room/" % roomAlias); } static const auto GetRoomIdByAliasJobName = QStringLiteral("GetRoomIdByAliasJob"); GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias) : BaseJob(HttpVerb::Get, GetRoomIdByAliasJobName, basePath % "/room/" % roomAlias, false) , d(new Private) {} GetRoomIdByAliasJob::~GetRoomIdByAliasJob() = default; const QString& GetRoomIdByAliasJob::roomId() const { return d->roomId; } const QStringList& GetRoomIdByAliasJob::servers() const { return d->servers; } BaseJob::Status GetRoomIdByAliasJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("room_id"_ls), d->roomId); fromJson(json.value("servers"_ls), d->servers); return Success; } QUrl DeleteRoomAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/room/" % roomAlias); } static const auto DeleteRoomAliasJobName = QStringLiteral("DeleteRoomAliasJob"); DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias) : BaseJob(HttpVerb::Delete, DeleteRoomAliasJobName, basePath % "/room/" % roomAlias) {} spectral/include/libQuotient/lib/csapi/account-data.cpp0000644000175000000620000000554113566674122023227 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "account-data.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); static const auto SetAccountDataJobName = QStringLiteral("SetAccountDataJob"); SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content) : BaseJob(HttpVerb::Put, SetAccountDataJobName, basePath % "/user/" % userId % "/account_data/" % type) { setRequestData(Data(toJson(content))); } QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/user/" % userId % "/account_data/" % type); } static const auto GetAccountDataJobName = QStringLiteral("GetAccountDataJob"); GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type) : BaseJob(HttpVerb::Get, GetAccountDataJobName, basePath % "/user/" % userId % "/account_data/" % type) {} static const auto SetAccountDataPerRoomJobName = QStringLiteral("SetAccountDataPerRoomJob"); SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content) : BaseJob(HttpVerb::Put, SetAccountDataPerRoomJobName, basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type) { setRequestData(Data(toJson(content))); } QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type); } static const auto GetAccountDataPerRoomJobName = QStringLiteral("GetAccountDataPerRoomJob"); GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type) : BaseJob(HttpVerb::Get, GetAccountDataPerRoomJobName, basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type) {} spectral/include/libQuotient/lib/csapi/directory.h0000644000175000000620000000476213566674122022341 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Create a new mapping from room alias to room ID. class SetRoomAliasJob : public BaseJob { public: /*! Create a new mapping from room alias to room ID. * \param roomAlias * The room alias to set. * \param roomId * The room ID to set. */ explicit SetRoomAliasJob(const QString& roomAlias, const QString& roomId); }; /// Get the room ID corresponding to this room alias. /*! * Requests that the server resolve a room alias to a room ID. * * The server will use the federation API to resolve the alias if the * domain part of the alias does not correspond to the server's own * domain. */ class GetRoomIdByAliasJob : public BaseJob { public: /*! Get the room ID corresponding to this room alias. * \param roomAlias * The room alias. */ explicit GetRoomIdByAliasJob(const QString& roomAlias); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetRoomIdByAliasJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); ~GetRoomIdByAliasJob() override; // Result properties /// The room ID for this room alias. const QString& roomId() const; /// A list of servers that are aware of this room alias. const QStringList& servers() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Remove a mapping of room alias to room ID. /*! * Remove a mapping of room alias to room ID. * * Servers may choose to implement additional access control checks here, for * instance that room aliases can only be deleted by their creator or a server * administrator. */ class DeleteRoomAliasJob : public BaseJob { public: /*! Remove a mapping of room alias to room ID. * \param roomAlias * The room alias to remove. */ explicit DeleteRoomAliasJob(const QString& roomAlias); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * DeleteRoomAliasJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomAlias); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/rooms.cpp0000644000175000000620000001573013566674122022024 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "rooms.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class GetOneRoomEventJob::Private { public: EventPtr data; }; QUrl GetOneRoomEventJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/rooms/" % roomId % "/event/" % eventId); } static const auto GetOneRoomEventJobName = QStringLiteral("GetOneRoomEventJob"); GetOneRoomEventJob::GetOneRoomEventJob(const QString& roomId, const QString& eventId) : BaseJob(HttpVerb::Get, GetOneRoomEventJobName, basePath % "/rooms/" % roomId % "/event/" % eventId) , d(new Private) {} GetOneRoomEventJob::~GetOneRoomEventJob() = default; EventPtr&& GetOneRoomEventJob::data() { return std::move(d->data); } BaseJob::Status GetOneRoomEventJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType, const QString& stateKey) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey); } static const auto GetRoomStateWithKeyJobName = QStringLiteral("GetRoomStateWithKeyJob"); GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey) : BaseJob(HttpVerb::Get, GetRoomStateWithKeyJobName, basePath % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey) {} QUrl GetRoomStateByTypeJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventType) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/rooms/" % roomId % "/state/" % eventType); } static const auto GetRoomStateByTypeJobName = QStringLiteral("GetRoomStateByTypeJob"); GetRoomStateByTypeJob::GetRoomStateByTypeJob(const QString& roomId, const QString& eventType) : BaseJob(HttpVerb::Get, GetRoomStateByTypeJobName, basePath % "/rooms/" % roomId % "/state/" % eventType) {} class GetRoomStateJob::Private { public: StateEvents data; }; QUrl GetRoomStateJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/rooms/" % roomId % "/state"); } static const auto GetRoomStateJobName = QStringLiteral("GetRoomStateJob"); GetRoomStateJob::GetRoomStateJob(const QString& roomId) : BaseJob(HttpVerb::Get, GetRoomStateJobName, basePath % "/rooms/" % roomId % "/state") , d(new Private) {} GetRoomStateJob::~GetRoomStateJob() = default; StateEvents&& GetRoomStateJob::data() { return std::move(d->data); } BaseJob::Status GetRoomStateJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } class GetMembersByRoomJob::Private { public: EventsArray chunk; }; BaseJob::Query queryToGetMembersByRoom(const QString& at, const QString& membership, const QString& notMembership) { BaseJob::Query _q; addParam(_q, QStringLiteral("at"), at); addParam(_q, QStringLiteral("membership"), membership); addParam(_q, QStringLiteral("not_membership"), notMembership); return _q; } QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& at, const QString& membership, const QString& notMembership) { return BaseJob::makeRequestUrl( std::move(baseUrl), basePath % "/rooms/" % roomId % "/members", queryToGetMembersByRoom(at, membership, notMembership)); } static const auto GetMembersByRoomJobName = QStringLiteral("GetMembersByRoomJob"); GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId, const QString& at, const QString& membership, const QString& notMembership) : BaseJob(HttpVerb::Get, GetMembersByRoomJobName, basePath % "/rooms/" % roomId % "/members", queryToGetMembersByRoom(at, membership, notMembership)) , d(new Private) {} GetMembersByRoomJob::~GetMembersByRoomJob() = default; EventsArray&& GetMembersByRoomJob::chunk() { return std::move(d->chunk); } BaseJob::Status GetMembersByRoomJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("chunk"_ls), d->chunk); return Success; } // Converters namespace Quotient { template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, GetJoinedMembersByRoomJob::RoomMember& result) { fromJson(jo.value("display_name"_ls), result.displayName); fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } }; } // namespace Quotient class GetJoinedMembersByRoomJob::Private { public: QHash joined; }; QUrl GetJoinedMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl( std::move(baseUrl), basePath % "/rooms/" % roomId % "/joined_members"); } static const auto GetJoinedMembersByRoomJobName = QStringLiteral("GetJoinedMembersByRoomJob"); GetJoinedMembersByRoomJob::GetJoinedMembersByRoomJob(const QString& roomId) : BaseJob(HttpVerb::Get, GetJoinedMembersByRoomJobName, basePath % "/rooms/" % roomId % "/joined_members") , d(new Private) {} GetJoinedMembersByRoomJob::~GetJoinedMembersByRoomJob() = default; const QHash& GetJoinedMembersByRoomJob::joined() const { return d->joined; } BaseJob::Status GetJoinedMembersByRoomJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("joined"_ls), d->joined); return Success; } spectral/include/libQuotient/lib/csapi/peeking_events.cpp0000644000175000000620000000365113566674122023672 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "peeking_events.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class PeekEventsJob::Private { public: QString begin; QString end; RoomEvents chunk; }; BaseJob::Query queryToPeekEvents(const QString& from, Omittable timeout, const QString& roomId) { BaseJob::Query _q; addParam(_q, QStringLiteral("from"), from); addParam(_q, QStringLiteral("timeout"), timeout); addParam(_q, QStringLiteral("room_id"), roomId); return _q; } QUrl PeekEventsJob::makeRequestUrl(QUrl baseUrl, const QString& from, Omittable timeout, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/events", queryToPeekEvents(from, timeout, roomId)); } static const auto PeekEventsJobName = QStringLiteral("PeekEventsJob"); PeekEventsJob::PeekEventsJob(const QString& from, Omittable timeout, const QString& roomId) : BaseJob(HttpVerb::Get, PeekEventsJobName, basePath % "/events", queryToPeekEvents(from, timeout, roomId)) , d(new Private) {} PeekEventsJob::~PeekEventsJob() = default; const QString& PeekEventsJob::begin() const { return d->begin; } const QString& PeekEventsJob::end() const { return d->end; } RoomEvents&& PeekEventsJob::chunk() { return std::move(d->chunk); } BaseJob::Status PeekEventsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("start"_ls), d->begin); fromJson(json.value("end"_ls), d->end); fromJson(json.value("chunk"_ls), d->chunk); return Success; } spectral/include/libQuotient/lib/csapi/peeking_events.h0000644000175000000620000000445413566674122023341 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "events/eventloader.h" #include "jobs/basejob.h" namespace Quotient { // Operations /// Listen on the event stream. /*! * This will listen for new events related to a particular room and return * them to the caller. This will block until an event is received, or until * the ``timeout`` is reached. * * This API is the same as the normal ``/events`` endpoint, but can be * called by users who have not joined the room. * * Note that the normal ``/events`` endpoint has been deprecated. This * API will also be deprecated at some point, but its replacement is not * yet known. */ class PeekEventsJob : public BaseJob { public: /*! Listen on the event stream. * \param from * The token to stream from. This token is either from a previous * request to this API or from the initial sync API. * \param timeout * The maximum time in milliseconds to wait for an event. * \param roomId * The room ID for which events should be returned. */ explicit PeekEventsJob(const QString& from = {}, Omittable timeout = none, const QString& roomId = {}); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * PeekEventsJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& from = {}, Omittable timeout = none, const QString& roomId = {}); ~PeekEventsJob() override; // Result properties /// A token which correlates to the first value in ``chunk``. This /// is usually the same token supplied to ``from=``. const QString& begin() const; /// A token which correlates to the last value in ``chunk``. This /// token should be used in the next request to ``/events``. const QString& end() const; /// An array of events. RoomEvents&& chunk(); protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/search.cpp0000644000175000000620000001311313566674122022123 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "search.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); // Converters namespace Quotient { template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const SearchJob::IncludeEventContext& pod) { addParam(jo, QStringLiteral("before_limit"), pod.beforeLimit); addParam(jo, QStringLiteral("after_limit"), pod.afterLimit); addParam(jo, QStringLiteral("include_profile"), pod.includeProfile); } }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const SearchJob::Group& pod) { addParam(jo, QStringLiteral("key"), pod.key); } }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const SearchJob::Groupings& pod) { addParam(jo, QStringLiteral("group_by"), pod.groupBy); } }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const SearchJob::RoomEventsCriteria& pod) { addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm); addParam(jo, QStringLiteral("keys"), pod.keys); addParam(jo, QStringLiteral("filter"), pod.filter); addParam(jo, QStringLiteral("order_by"), pod.orderBy); addParam(jo, QStringLiteral("event_context"), pod.eventContext); addParam(jo, QStringLiteral("include_state"), pod.includeState); addParam(jo, QStringLiteral("groupings"), pod.groupings); } }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const SearchJob::Categories& pod) { addParam(jo, QStringLiteral("room_events"), pod.roomEvents); } }; template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, SearchJob::UserProfile& result) { fromJson(jo.value("displayname"_ls), result.displayname); fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } }; template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, SearchJob::EventContext& result) { fromJson(jo.value("start"_ls), result.begin); fromJson(jo.value("end"_ls), result.end); fromJson(jo.value("profile_info"_ls), result.profileInfo); fromJson(jo.value("events_before"_ls), result.eventsBefore); fromJson(jo.value("events_after"_ls), result.eventsAfter); } }; template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, SearchJob::Result& result) { fromJson(jo.value("rank"_ls), result.rank); fromJson(jo.value("result"_ls), result.result); fromJson(jo.value("context"_ls), result.context); } }; template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, SearchJob::GroupValue& result) { fromJson(jo.value("next_batch"_ls), result.nextBatch); fromJson(jo.value("order"_ls), result.order); fromJson(jo.value("results"_ls), result.results); } }; template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, SearchJob::ResultRoomEvents& result) { fromJson(jo.value("count"_ls), result.count); fromJson(jo.value("highlights"_ls), result.highlights); fromJson(jo.value("results"_ls), result.results); fromJson(jo.value("state"_ls), result.state); fromJson(jo.value("groups"_ls), result.groups); fromJson(jo.value("next_batch"_ls), result.nextBatch); } }; template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, SearchJob::ResultCategories& result) { fromJson(jo.value("room_events"_ls), result.roomEvents); } }; } // namespace Quotient class SearchJob::Private { public: ResultCategories searchCategories; }; BaseJob::Query queryToSearch(const QString& nextBatch) { BaseJob::Query _q; addParam(_q, QStringLiteral("next_batch"), nextBatch); return _q; } static const auto SearchJobName = QStringLiteral("SearchJob"); SearchJob::SearchJob(const Categories& searchCategories, const QString& nextBatch) : BaseJob(HttpVerb::Post, SearchJobName, basePath % "/search", queryToSearch(nextBatch)) , d(new Private) { QJsonObject _data; addParam<>(_data, QStringLiteral("search_categories"), searchCategories); setRequestData(_data); } SearchJob::~SearchJob() = default; const SearchJob::ResultCategories& SearchJob::searchCategories() const { return d->searchCategories; } BaseJob::Status SearchJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("search_categories"_ls)) return { IncorrectResponse, "The key 'search_categories' not found in the response" }; fromJson(json.value("search_categories"_ls), d->searchCategories); return Success; } spectral/include/libQuotient/lib/csapi/search.h0000644000175000000620000001503213566674122021572 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/definitions/room_event_filter.h" #include "events/eventloader.h" #include "jobs/basejob.h" #include #include #include namespace Quotient { // Operations /// Perform a server-side search. /*! * Performs a full text search across different categories. */ class SearchJob : public BaseJob { public: // Inner data structures /// Configures whether any context for the eventsreturned are included in /// the response. struct IncludeEventContext { /// How many events before the result arereturned. By default, this is /// ``5``. Omittable beforeLimit; /// How many events after the result arereturned. By default, this is /// ``5``. Omittable afterLimit; /// Requests that the server returns thehistoric profile information for /// the usersthat sent the events that were returned.By default, this is /// ``false``. Omittable includeProfile; }; /// Configuration for group. struct Group { /// Key that defines the group. QString key; }; /// Requests that the server partitions the result setbased on the provided /// list of keys. struct Groupings { /// List of groups to request. QVector groupBy; }; /// Mapping of category name to search criteria. struct RoomEventsCriteria { /// The string to search events for QString searchTerm; /// The keys to search. Defaults to all. QStringList keys; /// This takes a `filter`_. Omittable filter; /// The order in which to search for results.By default, this is /// ``"rank"``. QString orderBy; /// Configures whether any context for the eventsreturned are included /// in the response. Omittable eventContext; /// Requests the server return the current state foreach room returned. Omittable includeState; /// Requests that the server partitions the result setbased on the /// provided list of keys. Omittable groupings; }; /// Describes which categories to search in and their criteria. struct Categories { /// Mapping of category name to search criteria. Omittable roomEvents; }; /// Performs a full text search across different categories. struct UserProfile { /// Performs a full text search across different categories. QString displayname; /// Performs a full text search across different categories. QString avatarUrl; }; /// Context for result, if requested. struct EventContext { /// Pagination token for the start of the chunk QString begin; /// Pagination token for the end of the chunk QString end; /// The historic profile information of theusers that sent the events /// returned.The ``string`` key is the user ID for whichthe profile /// belongs to. QHash profileInfo; /// Events just before the result. RoomEvents eventsBefore; /// Events just after the result. RoomEvents eventsAfter; }; /// The result object. struct Result { /// A number that describes how closely this result matches the search. /// Higher is closer. Omittable rank; /// The event that matched. RoomEventPtr result; /// Context for result, if requested. Omittable context; }; /// The results for a particular group value. struct GroupValue { /// Token that can be used to get the next batchof results in the group, /// by passing as the`next_batch` parameter to the next call. Ifthis /// field is absent, there are no moreresults in this group. QString nextBatch; /// Key that can be used to order differentgroups. Omittable order; /// Which results are in this group. QStringList results; }; /// Mapping of category name to search criteria. struct ResultRoomEvents { /// An approximate count of the total number of results found. Omittable count; /// List of words which should be highlighted, useful for stemming which /// may change the query terms. QStringList highlights; /// List of results in the requested order. std::vector results; /// The current state for every room in the results.This is included if /// the request had the``include_state`` key set with a value of /// ``true``.The ``string`` key is the room ID for which the /// ``StateEvent`` array belongs to. std::unordered_map state; /// Any groups that were requested.The outer ``string`` key is the group /// key requested (eg: ``room_id``or ``sender``). The inner ``string`` /// key is the grouped value (eg: a room's ID or a user's ID). QHash> groups; /// Token that can be used to get the next batch ofresults, by passing /// as the `next_batch` parameter tothe next call. If this field is /// absent, there are nomore results. QString nextBatch; }; /// Describes which categories to search in and their criteria. struct ResultCategories { /// Mapping of category name to search criteria. Omittable roomEvents; }; // Construction/destruction /*! Perform a server-side search. * \param searchCategories * Describes which categories to search in and their criteria. * \param nextBatch * The point to return events from. If given, this should be a * ``next_batch`` result from a previous call to this endpoint. */ explicit SearchJob(const Categories& searchCategories, const QString& nextBatch = {}); ~SearchJob() override; // Result properties /// Describes which categories to search in and their criteria. const ResultCategories& searchCategories() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/room_upgrades.h0000644000175000000620000000164213566674122023175 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Upgrades a room to a new room version. /*! * Upgrades the given room to a particular room version. */ class UpgradeRoomJob : public BaseJob { public: /*! Upgrades a room to a new room version. * \param roomId * The ID of the room to upgrade. * \param newVersion * The new version for the room. */ explicit UpgradeRoomJob(const QString& roomId, const QString& newVersion); ~UpgradeRoomJob() override; // Result properties /// The ID of the new room. const QString& replacementRoom() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/device_management.cpp0000644000175000000620000000601313566674122024312 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "device_management.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class GetDevicesJob::Private { public: QVector devices; }; QUrl GetDevicesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/devices"); } static const auto GetDevicesJobName = QStringLiteral("GetDevicesJob"); GetDevicesJob::GetDevicesJob() : BaseJob(HttpVerb::Get, GetDevicesJobName, basePath % "/devices") , d(new Private) {} GetDevicesJob::~GetDevicesJob() = default; const QVector& GetDevicesJob::devices() const { return d->devices; } BaseJob::Status GetDevicesJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("devices"_ls), d->devices); return Success; } class GetDeviceJob::Private { public: Device data; }; QUrl GetDeviceJob::makeRequestUrl(QUrl baseUrl, const QString& deviceId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/devices/" % deviceId); } static const auto GetDeviceJobName = QStringLiteral("GetDeviceJob"); GetDeviceJob::GetDeviceJob(const QString& deviceId) : BaseJob(HttpVerb::Get, GetDeviceJobName, basePath % "/devices/" % deviceId) , d(new Private) {} GetDeviceJob::~GetDeviceJob() = default; const Device& GetDeviceJob::data() const { return d->data; } BaseJob::Status GetDeviceJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } static const auto UpdateDeviceJobName = QStringLiteral("UpdateDeviceJob"); UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, const QString& displayName) : BaseJob(HttpVerb::Put, UpdateDeviceJobName, basePath % "/devices/" % deviceId) { QJsonObject _data; addParam(_data, QStringLiteral("display_name"), displayName); setRequestData(_data); } static const auto DeleteDeviceJobName = QStringLiteral("DeleteDeviceJob"); DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, const Omittable& auth) : BaseJob(HttpVerb::Delete, DeleteDeviceJobName, basePath % "/devices/" % deviceId) { QJsonObject _data; addParam(_data, QStringLiteral("auth"), auth); setRequestData(_data); } static const auto DeleteDevicesJobName = QStringLiteral("DeleteDevicesJob"); DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices, const Omittable& auth) : BaseJob(HttpVerb::Post, DeleteDevicesJobName, basePath % "/delete_devices") { QJsonObject _data; addParam<>(_data, QStringLiteral("devices"), devices); addParam(_data, QStringLiteral("auth"), auth); setRequestData(_data); } spectral/include/libQuotient/lib/csapi/whoami.cpp0000644000175000000620000000230613566674122022144 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "whoami.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class GetTokenOwnerJob::Private { public: QString userId; }; QUrl GetTokenOwnerJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/account/whoami"); } static const auto GetTokenOwnerJobName = QStringLiteral("GetTokenOwnerJob"); GetTokenOwnerJob::GetTokenOwnerJob() : BaseJob(HttpVerb::Get, GetTokenOwnerJobName, basePath % "/account/whoami") , d(new Private) {} GetTokenOwnerJob::~GetTokenOwnerJob() = default; const QString& GetTokenOwnerJob::userId() const { return d->userId; } BaseJob::Status GetTokenOwnerJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("user_id"_ls)) return { IncorrectResponse, "The key 'user_id' not found in the response" }; fromJson(json.value("user_id"_ls), d->userId); return Success; } spectral/include/libQuotient/lib/csapi/voip.h0000644000175000000620000000175113566674122021305 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Obtain TURN server credentials. /*! * This API provides credentials for the client to use when initiating * calls. */ class GetTurnServerJob : public BaseJob { public: explicit GetTurnServerJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetTurnServerJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); ~GetTurnServerJob() override; // Result properties /// The TURN server credentials. const QJsonObject& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/joining.h0000644000175000000620000001337613566674122021773 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Start the requesting user participating in a particular room. /*! * *Note that this API requires a room ID, not alias.* ``/join/{roomIdOrAlias}`` * *exists if you have a room alias.* * * This API starts a user participating in a particular room, if that user * is allowed to participate in that room. After this call, the client is * allowed to see all current state events in the room, and all subsequent * events associated with the room until the user leaves the room. * * After a user has joined a room, the room will appear as an entry in the * response of the |/initialSync|_ and |/sync|_ APIs. * * If a ``third_party_signed`` was supplied, the homeserver must verify * that it matches a pending ``m.room.third_party_invite`` event in the * room, and perform key validity checking if required by the event. */ class JoinRoomByIdJob : public BaseJob { public: // Inner data structures /// A signature of an ``m.third_party_invite`` token to prove that this user /// owns a third party identity which has been invited to the room. struct ThirdPartySigned { /// The Matrix ID of the user who issued the invite. QString sender; /// The Matrix ID of the invitee. QString mxid; /// The state key of the m.third_party_invite event. QString token; /// A signatures object containing a signature of the entire signed /// object. QJsonObject signatures; }; // Construction/destruction /*! Start the requesting user participating in a particular room. * \param roomId * The room identifier (not alias) to join. * \param thirdPartySigned * A signature of an ``m.third_party_invite`` token to prove that this * user owns a third party identity which has been invited to the room. */ explicit JoinRoomByIdJob( const QString& roomId, const Omittable& thirdPartySigned = none); ~JoinRoomByIdJob() override; // Result properties /// The joined room ID. const QString& roomId() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Start the requesting user participating in a particular room. /*! * *Note that this API takes either a room ID or alias, unlike* * ``/room/{roomId}/join``. * * This API starts a user participating in a particular room, if that user * is allowed to participate in that room. After this call, the client is * allowed to see all current state events in the room, and all subsequent * events associated with the room until the user leaves the room. * * After a user has joined a room, the room will appear as an entry in the * response of the |/initialSync|_ and |/sync|_ APIs. * * If a ``third_party_signed`` was supplied, the homeserver must verify * that it matches a pending ``m.room.third_party_invite`` event in the * room, and perform key validity checking if required by the event. */ class JoinRoomJob : public BaseJob { public: // Inner data structures /// *Note that this API takes either a room ID or alias, unlike* /// ``/room/{roomId}/join``.This API starts a user participating in a /// particular room, if that useris allowed to participate in that room. /// After this call, the client isallowed to see all current state events in /// the room, and all subsequentevents associated with the room until the /// user leaves the room.After a user has joined a room, the room will /// appear as an entry in theresponse of the |/initialSync|_ and |/sync|_ /// APIs.If a ``third_party_signed`` was supplied, the homeserver must /// verifythat it matches a pending ``m.room.third_party_invite`` event in /// theroom, and perform key validity checking if required by the event. struct Signed { /// The Matrix ID of the user who issued the invite. QString sender; /// The Matrix ID of the invitee. QString mxid; /// The state key of the m.third_party_invite event. QString token; /// A signatures object containing a signature of the entire signed /// object. QJsonObject signatures; }; /// A signature of an ``m.third_party_invite`` token to prove that this user /// owns a third party identity which has been invited to the room. struct ThirdPartySigned { /// A signature of an ``m.third_party_invite`` token to prove that this /// user owns a third party identity which has been invited to the room. Signed signedData; }; // Construction/destruction /*! Start the requesting user participating in a particular room. * \param roomIdOrAlias * The room identifier or alias to join. * \param serverName * The servers to attempt to join the room through. One of the servers * must be participating in the room. * \param thirdPartySigned * A signature of an ``m.third_party_invite`` token to prove that this * user owns a third party identity which has been invited to the room. */ explicit JoinRoomJob( const QString& roomIdOrAlias, const QStringList& serverName = {}, const Omittable& thirdPartySigned = none); ~JoinRoomJob() override; // Result properties /// The joined room ID. const QString& roomId() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/banning.cpp0000644000175000000620000000206513566674122022276 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "banning.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); static const auto BanJobName = QStringLiteral("BanJob"); BanJob::BanJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, BanJobName, basePath % "/rooms/" % roomId % "/ban") { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); addParam(_data, QStringLiteral("reason"), reason); setRequestData(_data); } static const auto UnbanJobName = QStringLiteral("UnbanJob"); UnbanJob::UnbanJob(const QString& roomId, const QString& userId) : BaseJob(HttpVerb::Post, UnbanJobName, basePath % "/rooms/" % roomId % "/unban") { QJsonObject _data; addParam<>(_data, QStringLiteral("user_id"), userId); setRequestData(_data); } spectral/include/libQuotient/lib/csapi/list_joined_rooms.h0000644000175000000620000000174013566674122024050 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Lists the user's current rooms. /*! * This API returns a list of the user's current rooms. */ class GetJoinedRoomsJob : public BaseJob { public: explicit GetJoinedRoomsJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetJoinedRoomsJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); ~GetJoinedRoomsJob() override; // Result properties /// The ID of each room in which the user has ``joined`` membership. const QStringList& joinedRooms() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/report_content.h0000644000175000000620000000176313566674122023400 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "jobs/basejob.h" namespace Quotient { // Operations /// Reports an event as inappropriate. /*! * Reports an event as inappropriate to the server, which may then notify * the appropriate people. */ class ReportContentJob : public BaseJob { public: /*! Reports an event as inappropriate. * \param roomId * The room in which the event being reported is located. * \param eventId * The event to report. * \param score * The score to rate this content as where -100 is most offensive * and 0 is inoffensive. * \param reason * The reason the content is being reported. May be blank. */ explicit ReportContentJob(const QString& roomId, const QString& eventId, int score, const QString& reason); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/typing.h0000644000175000000620000000227013566674122021637 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "jobs/basejob.h" namespace Quotient { // Operations /// Informs the server that the user has started or stopped typing. /*! * This tells the server that the user is typing for the next N * milliseconds where N is the value specified in the ``timeout`` key. * Alternatively, if ``typing`` is ``false``, it tells the server that the * user has stopped typing. */ class SetTypingJob : public BaseJob { public: /*! Informs the server that the user has started or stopped typing. * \param userId * The user who has started to type. * \param roomId * The room in which the user is typing. * \param typing * Whether the user is typing or not. If ``false``, the ``timeout`` * key can be omitted. * \param timeout * The length of time in milliseconds to mark this user as typing. */ explicit SetTypingJob(const QString& userId, const QString& roomId, bool typing, Omittable timeout = none); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/read_markers.h0000644000175000000620000000210513566674122022761 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Set the position of the read marker for a room. /*! * Sets the position of the read marker for a given room, and optionally * the read receipt's location. */ class SetReadMarkerJob : public BaseJob { public: /*! Set the position of the read marker for a room. * \param roomId * The room ID to set the read marker in for the user. * \param mFullyRead * The event ID the read marker should be located at. The * event MUST belong to the room. * \param mRead * The event ID to set the read receipt location at. This is * equivalent to calling ``/receipt/m.read/$elsewhere:example.org`` * and is provided here to save that extra call. */ explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead = {}); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/{{base}}.cpp.mustache0000644000175000000620000001352713566674122024371 0ustar dilingerstaff{{>preamble}} #include "{{filenameBase}}.h" {{^models}} #include "converters.h"{{/models}} {{#operations}} {{#producesNonJson?}} #include {{/producesNonJson?}} #include {{/operations}} using namespace Quotient; {{#models.model}} {{#in?}} void JsonObjectConverter<{{qualifiedName}}>::dumpTo(QJsonObject& jo, const {{qualifiedName}}& pod) { {{#propertyMap }} fillJson(jo, pod.{{nameCamelCase}}); {{/propertyMap}}{{#parents }} fillJson<{{name}}>(jo, pod); {{/parents}}{{#vars }} addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}}); {{/vars}} } {{/in?}}{{#out?}} void JsonObjectConverter<{{qualifiedName}}>::fillFrom({{>maybeCrefJsonObject}} jo, {{qualifiedName}}& result) { {{#parents }} fillFromJson<{{qualifiedName}}>(jo, result); {{/parents}}{{#vars }} fromJson(jo.{{>takeOrValue}}("{{baseName}}"_ls), result.{{nameCamelCase}}); {{/vars}}{{#propertyMap }} fromJson(jo, result.{{nameCamelCase}}); {{/propertyMap}} } {{/out?}} {{/models.model}} {{#operations}} static const auto basePath = QStringLiteral("{{basePathWithoutHost}}"); {{#operation}}{{#models}} // Converters namespace Quotient { {{#model}} template <> struct JsonObjectConverter<{{qualifiedName}}> { {{#in? }} static void dumpTo(QJsonObject& jo, const {{qualifiedName}}& pod) { {{#propertyMap }} fillJson(jo, pod.{{nameCamelCase}}); {{/propertyMap}}{{#parents }} fillJson<{{name}}>(jo, pod); {{/parents}}{{#vars }} addParam<{{^required?}}IfNotEmpty{{/required?}}>(jo, QStringLiteral("{{baseName}}"), pod.{{nameCamelCase}}); {{/vars}} } {{/in?}}{{#out? }} static void fillFrom({{>maybeCrefJsonObject}} jo, {{qualifiedName}}& result) { {{#parents }} fillFromJson<{{qualifiedName}}{{!of the parent!}}>(jo, result); {{/parents}}{{#vars }} fromJson(jo.{{>takeOrValue}}("{{baseName}}"_ls), result.{{nameCamelCase}}); {{/vars}}{{#propertyMap }} fromJson(jo, result.{{nameCamelCase}}); {{/propertyMap}} } {{/out?}} }; {{/model}} } // namespace QMatrixClient {{/models}} {{#responses}}{{#normalResponse?}}{{#allProperties?}} class {{camelCaseOperationId}}Job::Private { public:{{#allProperties}} {{>maybeOmittableType}} {{paramName}};{{/allProperties}} }; {{/allProperties?}}{{/normalResponse?}}{{/responses}} {{#queryParams?}} BaseJob::Query queryTo{{camelCaseOperationId}}({{#queryParams}}{{>joinedParamDef}}{{/queryParams}}) { BaseJob::Query _q;{{#queryParams}} addParam<{{^required?}}IfNotEmpty{{/required?}}>(_q, QStringLiteral("{{baseName}}"), {{paramName}});{{/queryParams}} return _q; } {{/queryParams?}} {{^bodyParams}} QUrl {{camelCaseOperationId}}Job::makeRequestUrl(QUrl baseUrl{{#allParams?}}, {{#allParams}}{{>joinedParamDef}}{{/allParams}}{{/allParams?}}) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath{{#pathParts}} % {{_}}{{/pathParts}}{{#queryParams?}}, queryTo{{camelCaseOperationId}}({{>passQueryParams}}){{/queryParams?}}); } {{/bodyParams}} static const auto {{camelCaseOperationId}}JobName = QStringLiteral("{{camelCaseOperationId}}Job"); {{camelCaseOperationId}}Job::{{camelCaseOperationId}}Job({{#allParams}}{{>joinedParamDef}}{{/allParams}}) : BaseJob(HttpVerb::{{#_cap}}{{#_tolower}}{{httpMethod}}{{/_tolower}}{{/_cap}}, {{camelCaseOperationId}}JobName, basePath{{#pathParts}} % {{_}}{{/pathParts}}{{#queryParams?}}, queryTo{{camelCaseOperationId}}({{>passQueryParams}}){{/queryParams?}}{{#skipAuth}}{{#queryParams?}}, {}{{/queryParams?}}, false{{/skipAuth}}){{#responses}}{{#normalResponse?}}{{#allProperties?}} , d(new Private){{/allProperties?}}{{/normalResponse?}}{{/responses}} { {{#headerParams?}}{{#headerParams }} setRequestHeader("{{baseName}}", {{paramName}}.toLatin1());{{/headerParams}} {{/headerParams?}}{{#bodyParams? }}{{#inlineBody }} setRequestData(Data({{#consumesNonJson?}}{{nameCamelCase}}{{/consumesNonJson? }}{{^consumesNonJson?}}toJson({{nameCamelCase}}){{/consumesNonJson?}})); {{/inlineBody}}{{^inlineBody }} QJsonObject _data;{{#bodyParams}} addParam<{{^required?}}IfNotEmpty{{/required?}}>(_data, QStringLiteral("{{baseName}}"), {{paramName}});{{/bodyParams}} setRequestData(_data); {{/inlineBody}}{{/bodyParams? }}{{#producesNonJson? }} setExpectedContentTypes({ {{#produces}}"{{_}}"{{>cjoin}}{{/produces}} }); {{/producesNonJson?}} } {{#responses}}{{#normalResponse?}}{{#allProperties?}} {{camelCaseOperationId}}Job::~{{camelCaseOperationId}}Job() = default; {{#allProperties}} {{>qualifiedMaybeCrefType}} {{camelCaseOperationId}}Job::{{paramName}}(){{^moveOnly}} const{{/moveOnly}} { return {{#moveOnly}}std::move({{/moveOnly}}d->{{paramName}}{{#moveOnly}}){{/moveOnly}}; } {{/allProperties}} {{#producesNonJson?}} BaseJob::Status {{camelCaseOperationId}}Job::parseReply(QNetworkReply* reply) { {{#headers}}d->{{paramName}} = reply->rawHeader("{{baseName}}");{{! We don't check for required headers yet }} {{/headers}}{{#properties}}d->{{paramName}} = reply;{{/properties}} return Success; } {{/producesNonJson?}}{{^producesNonJson?}} BaseJob::Status {{camelCaseOperationId}}Job::parseJson(const QJsonDocument& data) { {{#inlineResponse }} fromJson(data, d->{{paramName}}); {{/inlineResponse}}{{^inlineResponse }} auto json = data.object(); {{# properties}}{{#required? }} if (!json.contains("{{baseName}}"_ls)) return { IncorrectResponse, "The key '{{baseName}}' not found in the response" }; {{/required? }} fromJson(json.value("{{baseName}}"_ls), d->{{paramName}}); {{/ properties}} {{/inlineResponse }} return Success; } {{/producesNonJson?}} {{/allProperties?}}{{/normalResponse?}}{{/responses}} {{/operation}}{{/operations}} spectral/include/libQuotient/lib/csapi/admin.cpp0000644000175000000620000000420113566674122021744 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "admin.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); // Converters namespace Quotient { template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, GetWhoIsJob::ConnectionInfo& result) { fromJson(jo.value("ip"_ls), result.ip); fromJson(jo.value("last_seen"_ls), result.lastSeen); fromJson(jo.value("user_agent"_ls), result.userAgent); } }; template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, GetWhoIsJob::SessionInfo& result) { fromJson(jo.value("connections"_ls), result.connections); } }; template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, GetWhoIsJob::DeviceInfo& result) { fromJson(jo.value("sessions"_ls), result.sessions); } }; } // namespace Quotient class GetWhoIsJob::Private { public: QString userId; QHash devices; }; QUrl GetWhoIsJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/admin/whois/" % userId); } static const auto GetWhoIsJobName = QStringLiteral("GetWhoIsJob"); GetWhoIsJob::GetWhoIsJob(const QString& userId) : BaseJob(HttpVerb::Get, GetWhoIsJobName, basePath % "/admin/whois/" % userId) , d(new Private) {} GetWhoIsJob::~GetWhoIsJob() = default; const QString& GetWhoIsJob::userId() const { return d->userId; } const QHash& GetWhoIsJob::devices() const { return d->devices; } BaseJob::Status GetWhoIsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("user_id"_ls), d->userId); fromJson(json.value("devices"_ls), d->devices); return Success; } spectral/include/libQuotient/lib/csapi/pusher.cpp0000644000175000000620000000674313566674122022177 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "pusher.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); // Converters namespace Quotient { template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, GetPushersJob::PusherData& result) { fromJson(jo.value("url"_ls), result.url); fromJson(jo.value("format"_ls), result.format); } }; template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, GetPushersJob::Pusher& result) { fromJson(jo.value("pushkey"_ls), result.pushkey); fromJson(jo.value("kind"_ls), result.kind); fromJson(jo.value("app_id"_ls), result.appId); fromJson(jo.value("app_display_name"_ls), result.appDisplayName); fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); fromJson(jo.value("profile_tag"_ls), result.profileTag); fromJson(jo.value("lang"_ls), result.lang); fromJson(jo.value("data"_ls), result.data); } }; } // namespace Quotient class GetPushersJob::Private { public: QVector pushers; }; QUrl GetPushersJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/pushers"); } static const auto GetPushersJobName = QStringLiteral("GetPushersJob"); GetPushersJob::GetPushersJob() : BaseJob(HttpVerb::Get, GetPushersJobName, basePath % "/pushers") , d(new Private) {} GetPushersJob::~GetPushersJob() = default; const QVector& GetPushersJob::pushers() const { return d->pushers; } BaseJob::Status GetPushersJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("pushers"_ls), d->pushers); return Success; } // Converters namespace Quotient { template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const PostPusherJob::PusherData& pod) { addParam(jo, QStringLiteral("url"), pod.url); addParam(jo, QStringLiteral("format"), pod.format); } }; } // namespace Quotient static const auto PostPusherJobName = QStringLiteral("PostPusherJob"); PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag, Omittable append) : BaseJob(HttpVerb::Post, PostPusherJobName, basePath % "/pushers/set") { QJsonObject _data; addParam<>(_data, QStringLiteral("pushkey"), pushkey); addParam<>(_data, QStringLiteral("kind"), kind); addParam<>(_data, QStringLiteral("app_id"), appId); addParam<>(_data, QStringLiteral("app_display_name"), appDisplayName); addParam<>(_data, QStringLiteral("device_display_name"), deviceDisplayName); addParam(_data, QStringLiteral("profile_tag"), profileTag); addParam<>(_data, QStringLiteral("lang"), lang); addParam<>(_data, QStringLiteral("data"), data); addParam(_data, QStringLiteral("append"), append); setRequestData(_data); } spectral/include/libQuotient/lib/csapi/redaction.h0000644000175000000620000000304013566674122022271 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Strips all non-integrity-critical information out of an event. /*! * Strips all information out of an event which isn't critical to the * integrity of the server-side representation of the room. * * This cannot be undone. * * Users may redact their own events, and any user with a power level * greater than or equal to the `redact` power level of the room may * redact events there. */ class RedactEventJob : public BaseJob { public: /*! Strips all non-integrity-critical information out of an event. * \param roomId * The room from which to redact the event. * \param eventId * The ID of the event to redact * \param txnId * The transaction ID for this event. Clients should generate a * unique ID; it will be used by the server to ensure idempotency of * requests. \param reason The reason for the event being redacted. */ explicit RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason = {}); ~RedactEventJob() override; // Result properties /// A unique identifier for the event. const QString& eventId() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/room_state.cpp0000644000175000000620000000406113566674122023034 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "room_state.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class SetRoomStateWithKeyJob::Private { public: QString eventId; }; static const auto SetRoomStateWithKeyJobName = QStringLiteral("SetRoomStateWithKeyJob"); SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey, const QJsonObject& body) : BaseJob(HttpVerb::Put, SetRoomStateWithKeyJobName, basePath % "/rooms/" % roomId % "/state/" % eventType % "/" % stateKey) , d(new Private) { setRequestData(Data(toJson(body))); } SetRoomStateWithKeyJob::~SetRoomStateWithKeyJob() = default; const QString& SetRoomStateWithKeyJob::eventId() const { return d->eventId; } BaseJob::Status SetRoomStateWithKeyJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("event_id"_ls), d->eventId); return Success; } class SetRoomStateJob::Private { public: QString eventId; }; static const auto SetRoomStateJobName = QStringLiteral("SetRoomStateJob"); SetRoomStateJob::SetRoomStateJob(const QString& roomId, const QString& eventType, const QJsonObject& body) : BaseJob(HttpVerb::Put, SetRoomStateJobName, basePath % "/rooms/" % roomId % "/state/" % eventType) , d(new Private) { setRequestData(Data(toJson(body))); } SetRoomStateJob::~SetRoomStateJob() = default; const QString& SetRoomStateJob::eventId() const { return d->eventId; } BaseJob::Status SetRoomStateJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("event_id"_ls), d->eventId); return Success; } spectral/include/libQuotient/lib/csapi/profile.cpp0000644000175000000620000000752513566674122022330 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "profile.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); static const auto SetDisplayNameJobName = QStringLiteral("SetDisplayNameJob"); SetDisplayNameJob::SetDisplayNameJob(const QString& userId, const QString& displayname) : BaseJob(HttpVerb::Put, SetDisplayNameJobName, basePath % "/profile/" % userId % "/displayname") { QJsonObject _data; addParam(_data, QStringLiteral("displayname"), displayname); setRequestData(_data); } class GetDisplayNameJob::Private { public: QString displayname; }; QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl( std::move(baseUrl), basePath % "/profile/" % userId % "/displayname"); } static const auto GetDisplayNameJobName = QStringLiteral("GetDisplayNameJob"); GetDisplayNameJob::GetDisplayNameJob(const QString& userId) : BaseJob(HttpVerb::Get, GetDisplayNameJobName, basePath % "/profile/" % userId % "/displayname", false) , d(new Private) {} GetDisplayNameJob::~GetDisplayNameJob() = default; const QString& GetDisplayNameJob::displayname() const { return d->displayname; } BaseJob::Status GetDisplayNameJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("displayname"_ls), d->displayname); return Success; } static const auto SetAvatarUrlJobName = QStringLiteral("SetAvatarUrlJob"); SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QString& avatarUrl) : BaseJob(HttpVerb::Put, SetAvatarUrlJobName, basePath % "/profile/" % userId % "/avatar_url") { QJsonObject _data; addParam(_data, QStringLiteral("avatar_url"), avatarUrl); setRequestData(_data); } class GetAvatarUrlJob::Private { public: QString avatarUrl; }; QUrl GetAvatarUrlJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl( std::move(baseUrl), basePath % "/profile/" % userId % "/avatar_url"); } static const auto GetAvatarUrlJobName = QStringLiteral("GetAvatarUrlJob"); GetAvatarUrlJob::GetAvatarUrlJob(const QString& userId) : BaseJob(HttpVerb::Get, GetAvatarUrlJobName, basePath % "/profile/" % userId % "/avatar_url", false) , d(new Private) {} GetAvatarUrlJob::~GetAvatarUrlJob() = default; const QString& GetAvatarUrlJob::avatarUrl() const { return d->avatarUrl; } BaseJob::Status GetAvatarUrlJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("avatar_url"_ls), d->avatarUrl); return Success; } class GetUserProfileJob::Private { public: QString avatarUrl; QString displayname; }; QUrl GetUserProfileJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/profile/" % userId); } static const auto GetUserProfileJobName = QStringLiteral("GetUserProfileJob"); GetUserProfileJob::GetUserProfileJob(const QString& userId) : BaseJob(HttpVerb::Get, GetUserProfileJobName, basePath % "/profile/" % userId, false) , d(new Private) {} GetUserProfileJob::~GetUserProfileJob() = default; const QString& GetUserProfileJob::avatarUrl() const { return d->avatarUrl; } const QString& GetUserProfileJob::displayname() const { return d->displayname; } BaseJob::Status GetUserProfileJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("avatar_url"_ls), d->avatarUrl); fromJson(json.value("displayname"_ls), d->displayname); return Success; } spectral/include/libQuotient/lib/csapi/notifications.h0000644000175000000620000000601113566674122023173 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "events/eventloader.h" #include "jobs/basejob.h" #include #include #include namespace Quotient { // Operations /// Gets a list of events that the user has been notified about /*! * This API is used to paginate through the list of events that the * user has been, or would have been notified about. */ class GetNotificationsJob : public BaseJob { public: // Inner data structures /// This API is used to paginate through the list of events that theuser has /// been, or would have been notified about. struct Notification { /// The action(s) to perform when the conditions for this rule are /// met.See `Push Rules: API`_. QVector actions; /// The Event object for the event that triggered the notification. EventPtr event; /// The profile tag of the rule that matched this event. QString profileTag; /// Indicates whether the user has sent a read receipt indicatingthat /// they have read this message. bool read; /// The ID of the room in which the event was posted. QString roomId; /// The unix timestamp at which the event notification was sent,in /// milliseconds. int ts; }; // Construction/destruction /*! Gets a list of events that the user has been notified about * \param from * Pagination token given to retrieve the next set of events. * \param limit * Limit on the number of events to return in this request. * \param only * Allows basic filtering of events returned. Supply ``highlight`` * to return only events where the notification had the highlight * tweak set. */ explicit GetNotificationsJob(const QString& from = {}, Omittable limit = none, const QString& only = {}); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetNotificationsJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& from = {}, Omittable limit = none, const QString& only = {}); ~GetNotificationsJob() override; // Result properties /// The token to supply in the ``from`` param of the next /// ``/notifications`` request in order to request more /// events. If this is absent, there are no more results. const QString& nextToken() const; /// The list of events that triggered notifications. std::vector&& notifications(); protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/report_content.cpp0000644000175000000620000000151113566674122023722 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "report_content.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); static const auto ReportContentJobName = QStringLiteral("ReportContentJob"); ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId, int score, const QString& reason) : BaseJob(HttpVerb::Post, ReportContentJobName, basePath % "/rooms/" % roomId % "/report/" % eventId) { QJsonObject _data; addParam<>(_data, QStringLiteral("score"), score); addParam<>(_data, QStringLiteral("reason"), reason); setRequestData(_data); } spectral/include/libQuotient/lib/csapi/room_upgrades.cpp0000644000175000000620000000241413566674122023526 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "room_upgrades.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class UpgradeRoomJob::Private { public: QString replacementRoom; }; static const auto UpgradeRoomJobName = QStringLiteral("UpgradeRoomJob"); UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion) : BaseJob(HttpVerb::Post, UpgradeRoomJobName, basePath % "/rooms/" % roomId % "/upgrade") , d(new Private) { QJsonObject _data; addParam<>(_data, QStringLiteral("new_version"), newVersion); setRequestData(_data); } UpgradeRoomJob::~UpgradeRoomJob() = default; const QString& UpgradeRoomJob::replacementRoom() const { return d->replacementRoom; } BaseJob::Status UpgradeRoomJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("replacement_room"_ls)) return { IncorrectResponse, "The key 'replacement_room' not found in the response" }; fromJson(json.value("replacement_room"_ls), d->replacementRoom); return Success; } spectral/include/libQuotient/lib/csapi/create_room.h0000644000175000000620000002641513566674122022633 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "jobs/basejob.h" #include #include namespace Quotient { // Operations /// Create a new room /*! * Create a new room with various configuration options. * * The server MUST apply the normal state resolution rules when creating * the new room, including checking power levels for each event. It MUST * apply the events implied by the request in the following order: * * 0. A default ``m.room.power_levels`` event, giving the room creator * (and not other members) permission to send state events. Overridden * by the ``power_level_content_override`` parameter. * * 1. Events set by the ``preset``. Currently these are the * ``m.room.join_rules``, * ``m.room.history_visibility``, and ``m.room.guest_access`` state events. * * 2. Events listed in ``initial_state``, in the order that they are * listed. * * 3. Events implied by ``name`` and ``topic`` (``m.room.name`` and * ``m.room.topic`` state events). * * 4. Invite events implied by ``invite`` and ``invite_3pid`` (``m.room.member`` * with * ``membership: invite`` and ``m.room.third_party_invite``). * * The available presets do the following with respect to room state: * * ======================== ============== ====================== * ================ ========= Preset ``join_rules`` * ``history_visibility`` ``guest_access`` Other * ======================== ============== ====================== * ================ ========= * ``private_chat`` ``invite`` ``shared`` ``can_join`` * ``trusted_private_chat`` ``invite`` ``shared`` ``can_join`` All * invitees are given the same power level as the room creator. * ``public_chat`` ``public`` ``shared`` ``forbidden`` * ======================== ============== ====================== * ================ ========= * * The server will create a ``m.room.create`` event in the room with the * requesting user as the creator, alongside other keys provided in the * ``creation_content``. */ class CreateRoomJob : public BaseJob { public: // Inner data structures /// Create a new room with various configuration options.The server MUST /// apply the normal state resolution rules when creatingthe new room, /// including checking power levels for each event. It MUSTapply the events /// implied by the request in the following order:0. A default /// ``m.room.power_levels`` event, giving the room creator (and not other /// members) permission to send state events. Overridden by the /// ``power_level_content_override`` parameter.1. Events set by the /// ``preset``. Currently these are the ``m.room.join_rules``, /// ``m.room.history_visibility``, and ``m.room.guest_access`` state /// events.2. Events listed in ``initial_state``, in the order that they are /// listed.3. Events implied by ``name`` and ``topic`` (``m.room.name`` and /// ``m.room.topic`` state events).4. Invite events implied by ``invite`` /// and ``invite_3pid`` (``m.room.member`` with ``membership: invite`` and /// ``m.room.third_party_invite``).The available presets do the following /// with respect to room state:======================== ============== /// ====================== ================ ========= Preset /// ``join_rules`` ``history_visibility`` ``guest_access`` /// Other======================== ============== ====================== /// ================ =========``private_chat`` ``invite`` /// ``shared`` ``can_join````trusted_private_chat`` ``invite`` /// ``shared`` ``can_join`` All invitees are given the /// same power level as the room creator.``public_chat`` ``public`` /// ``shared`` ``forbidden``======================== /// ============== ====================== ================ =========The /// server will create a ``m.room.create`` event in the room with /// therequesting user as the creator, alongside other keys provided in /// the``creation_content``. struct Invite3pid { /// The hostname+port of the identity server which should be used for /// third party identifier lookups. QString idServer; /// The kind of address being passed in the address field, for example /// ``email``. QString medium; /// The invitee's third party identifier. QString address; }; /// Create a new room with various configuration options.The server MUST /// apply the normal state resolution rules when creatingthe new room, /// including checking power levels for each event. It MUSTapply the events /// implied by the request in the following order:0. A default /// ``m.room.power_levels`` event, giving the room creator (and not other /// members) permission to send state events. Overridden by the /// ``power_level_content_override`` parameter.1. Events set by the /// ``preset``. Currently these are the ``m.room.join_rules``, /// ``m.room.history_visibility``, and ``m.room.guest_access`` state /// events.2. Events listed in ``initial_state``, in the order that they are /// listed.3. Events implied by ``name`` and ``topic`` (``m.room.name`` and /// ``m.room.topic`` state events).4. Invite events implied by ``invite`` /// and ``invite_3pid`` (``m.room.member`` with ``membership: invite`` and /// ``m.room.third_party_invite``).The available presets do the following /// with respect to room state:======================== ============== /// ====================== ================ ========= Preset /// ``join_rules`` ``history_visibility`` ``guest_access`` /// Other======================== ============== ====================== /// ================ =========``private_chat`` ``invite`` /// ``shared`` ``can_join````trusted_private_chat`` ``invite`` /// ``shared`` ``can_join`` All invitees are given the /// same power level as the room creator.``public_chat`` ``public`` /// ``shared`` ``forbidden``======================== /// ============== ====================== ================ =========The /// server will create a ``m.room.create`` event in the room with /// therequesting user as the creator, alongside other keys provided in /// the``creation_content``. struct StateEvent { /// The type of event to send. QString type; /// The state_key of the state event. Defaults to an empty string. QString stateKey; /// The content of the event. QJsonObject content; }; // Construction/destruction /*! Create a new room * \param visibility * A ``public`` visibility indicates that the room will be shown * in the published room list. A ``private`` visibility will hide * the room from the published room list. Rooms default to * ``private`` visibility if this key is not included. NB: This * should not be confused with ``join_rules`` which also uses the * word ``public``. * \param roomAliasName * The desired room alias **local part**. If this is included, a * room alias will be created and mapped to the newly created * room. The alias will belong on the *same* homeserver which * created the room. For example, if this was set to "foo" and * sent to the homeserver "example.com" the complete room alias * would be ``#foo:example.com``. * * The complete room alias will become the canonical alias for * the room. * \param name * If this is included, an ``m.room.name`` event will be sent * into the room to indicate the name of the room. See Room * Events for more information on ``m.room.name``. * \param topic * If this is included, an ``m.room.topic`` event will be sent * into the room to indicate the topic for the room. See Room * Events for more information on ``m.room.topic``. * \param invite * A list of user IDs to invite to the room. This will tell the * server to invite everyone in the list to the newly created room. * \param invite3pid * A list of objects representing third party IDs to invite into * the room. * \param roomVersion * The room version to set for the room. If not provided, the homeserver * is to use its configured default. If provided, the homeserver will return * a 400 error with the errcode ``M_UNSUPPORTED_ROOM_VERSION`` if it does * not support the room version. \param creationContent Extra keys, such as * ``m.federate``, to be added to the content of the `m.room.create`_ event. * The server will clobber the following keys: ``creator``, * ``room_version``. Future versions of the specification may allow the * server to clobber other keys. \param initialState A list of state events * to set in the new room. This allows the user to override the default * state events set in the new room. The expected format of the state events * are an object with type, state_key and content keys set. * * Takes precedence over events set by ``preset``, but gets * overriden by ``name`` and ``topic`` keys. * \param preset * Convenience parameter for setting various default state events * based on a preset. * * If unspecified, the server should use the ``visibility`` to determine * which preset to use. A visbility of ``public`` equates to a preset of * ``public_chat`` and ``private`` visibility equates to a preset of * ``private_chat``. * \param isDirect * This flag makes the server set the ``is_direct`` flag on the * ``m.room.member`` events sent to the users in ``invite`` and * ``invite_3pid``. See `Direct Messaging`_ for more information. * \param powerLevelContentOverride * The power level content to override in the default power level * event. This object is applied on top of the generated * `m.room.power_levels`_ event content prior to it being sent to the room. * Defaults to overriding nothing. */ explicit CreateRoomJob(const QString& visibility = {}, const QString& roomAliasName = {}, const QString& name = {}, const QString& topic = {}, const QStringList& invite = {}, const QVector& invite3pid = {}, const QString& roomVersion = {}, const QJsonObject& creationContent = {}, const QVector& initialState = {}, const QString& preset = {}, Omittable isDirect = none, const QJsonObject& powerLevelContentOverride = {}); ~CreateRoomJob() override; // Result properties /// The created room's ID. const QString& roomId() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/login.h0000644000175000000620000001170413566674122021437 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/definitions/user_identifier.h" #include "csapi/definitions/wellknown/full.h" #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Get the supported login types to authenticate users /*! * Gets the homeserver's supported login types to authenticate users. Clients * should pick one of these and supply it as the ``type`` when logging in. */ class GetLoginFlowsJob : public BaseJob { public: // Inner data structures /// Gets the homeserver's supported login types to authenticate users. /// Clientsshould pick one of these and supply it as the ``type`` when /// logging in. struct LoginFlow { /// The login type. This is supplied as the ``type`` whenlogging in. QString type; }; // Construction/destruction explicit GetLoginFlowsJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetLoginFlowsJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); ~GetLoginFlowsJob() override; // Result properties /// The homeserver's supported login types const QVector& flows() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Authenticates the user. /*! * Authenticates the user, and issues an access token they can * use to authorize themself in subsequent requests. * * If the client does not supply a ``device_id``, the server must * auto-generate one. * * The returned access token must be associated with the ``device_id`` * supplied by the client or generated by the server. The server may * invalidate any access token previously associated with that device. See * `Relationship between access tokens and devices`_. */ class LoginJob : public BaseJob { public: /*! Authenticates the user. * \param type * The login type being used. * \param identifier * Identification information for the user. * \param password * Required when ``type`` is ``m.login.password``. The user's * password. * \param token * Required when ``type`` is ``m.login.token``. Part of `Token-based`_ * login. \param deviceId ID of the client device. If this does not * correspond to a known client device, a new device will be created. The * server will auto-generate a device_id if this is not specified. \param * initialDeviceDisplayName A display name to assign to the newly-created * device. Ignored if ``device_id`` corresponds to a known device. \param * user The fully qualified user ID or just local part of the user ID, to * log in. Deprecated in favour of ``identifier``. \param medium When * logging in using a third party identifier, the medium of the identifier. * Must be 'email'. Deprecated in favour of ``identifier``. \param address * Third party identifier for the user. Deprecated in favour of * ``identifier``. */ explicit LoginJob(const QString& type, const Omittable& identifier = none, const QString& password = {}, const QString& token = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, const QString& user = {}, const QString& medium = {}, const QString& address = {}); ~LoginJob() override; // Result properties /// The fully-qualified Matrix ID that has been registered. const QString& userId() const; /// An access token for the account. /// This access token can then be used to authorize other requests. const QString& accessToken() const; /// The server_name of the homeserver on which the account has /// been registered. /// /// **Deprecated**. Clients should extract the server_name from /// ``user_id`` (by splitting at the first colon) if they require /// it. Note also that ``homeserver`` is not spelt this way. const QString& homeServer() const; /// ID of the logged-in device. Will be the same as the /// corresponding parameter in the request, if one was specified. const QString& deviceId() const; /// Optional client configuration provided by the server. If present, /// clients SHOULD use the provided object to reconfigure themselves, /// optionally validating the URLs within. This object takes the same /// form as the one returned from .well-known autodiscovery. const Omittable& wellKnown() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/list_public_rooms.h0000644000175000000620000001346613566674122024066 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/definitions/public_rooms_response.h" #include "jobs/basejob.h" namespace Quotient { // Operations /// Gets the visibility of a room in the directory /*! * Gets the visibility of a given room on the server's public room directory. */ class GetRoomVisibilityOnDirectoryJob : public BaseJob { public: /*! Gets the visibility of a room in the directory * \param roomId * The room ID. */ explicit GetRoomVisibilityOnDirectoryJob(const QString& roomId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetRoomVisibilityOnDirectoryJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); ~GetRoomVisibilityOnDirectoryJob() override; // Result properties /// The visibility of the room in the directory. const QString& visibility() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Sets the visibility of a room in the room directory /*! * Sets the visibility of a given room in the server's public room * directory. * * Servers may choose to implement additional access control checks * here, for instance that room visibility can only be changed by * the room creator or a server administrator. */ class SetRoomVisibilityOnDirectoryJob : public BaseJob { public: /*! Sets the visibility of a room in the room directory * \param roomId * The room ID. * \param visibility * The new visibility setting for the room. * Defaults to 'public'. */ explicit SetRoomVisibilityOnDirectoryJob(const QString& roomId, const QString& visibility = {}); }; /// Lists the public rooms on the server. /*! * Lists the public rooms on the server. * * This API returns paginated responses. The rooms are ordered by the number * of joined members, with the largest rooms first. */ class GetPublicRoomsJob : public BaseJob { public: /*! Lists the public rooms on the server. * \param limit * Limit the number of results returned. * \param since * A pagination token from a previous request, allowing clients to * get the next (or previous) batch of rooms. * The direction of pagination is specified solely by which token * is supplied, rather than via an explicit flag. * \param server * The server to fetch the public room lists from. Defaults to the * local server. */ explicit GetPublicRoomsJob(Omittable limit = none, const QString& since = {}, const QString& server = {}); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetPublicRoomsJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, Omittable limit = none, const QString& since = {}, const QString& server = {}); ~GetPublicRoomsJob() override; // Result properties /// A list of the rooms on the server. const PublicRoomsResponse& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Lists the public rooms on the server with optional filter. /*! * Lists the public rooms on the server, with optional filter. * * This API returns paginated responses. The rooms are ordered by the number * of joined members, with the largest rooms first. */ class QueryPublicRoomsJob : public BaseJob { public: // Inner data structures /// Filter to apply to the results. struct Filter { /// A string to search for in the room metadata, e.g. name,topic, /// canonical alias etc. (Optional). QString genericSearchTerm; }; // Construction/destruction /*! Lists the public rooms on the server with optional filter. * \param server * The server to fetch the public room lists from. Defaults to the * local server. * \param limit * Limit the number of results returned. * \param since * A pagination token from a previous request, allowing clients * to get the next (or previous) batch of rooms. The direction * of pagination is specified solely by which token is supplied, * rather than via an explicit flag. * \param filter * Filter to apply to the results. * \param includeAllNetworks * Whether or not to include all known networks/protocols from * application services on the homeserver. Defaults to false. * \param thirdPartyInstanceId * The specific third party network/protocol to request from the * homeserver. Can only be used if ``include_all_networks`` is false. */ explicit QueryPublicRoomsJob(const QString& server = {}, Omittable limit = none, const QString& since = {}, const Omittable& filter = none, Omittable includeAllNetworks = none, const QString& thirdPartyInstanceId = {}); ~QueryPublicRoomsJob() override; // Result properties /// A list of the rooms on the server. const PublicRoomsResponse& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/tags.h0000644000175000000620000000644413566674122021272 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "jobs/basejob.h" #include #include namespace Quotient { // Operations /// List the tags for a room. /*! * List the tags set by a user on a room. */ class GetRoomTagsJob : public BaseJob { public: // Inner data structures /// List the tags set by a user on a room. struct Tag { /// A number in a range ``[0,1]`` describing a relativeposition of the /// room under the given tag. Omittable order; /// List the tags set by a user on a room. QVariantHash additionalProperties; }; // Construction/destruction /*! List the tags for a room. * \param userId * The id of the user to get tags for. The access token must be * authorized to make requests for this user ID. * \param roomId * The ID of the room to get tags for. */ explicit GetRoomTagsJob(const QString& userId, const QString& roomId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetRoomTagsJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId); ~GetRoomTagsJob() override; // Result properties /// List the tags set by a user on a room. const QHash& tags() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Add a tag to a room. /*! * Add a tag to the room. */ class SetRoomTagJob : public BaseJob { public: /*! Add a tag to a room. * \param userId * The id of the user to add a tag for. The access token must be * authorized to make requests for this user ID. * \param roomId * The ID of the room to add a tag to. * \param tag * The tag to add. * \param order * A number in a range ``[0,1]`` describing a relative * position of the room under the given tag. */ explicit SetRoomTagJob(const QString& userId, const QString& roomId, const QString& tag, Omittable order = none); }; /// Remove a tag from the room. /*! * Remove a tag from the room. */ class DeleteRoomTagJob : public BaseJob { public: /*! Remove a tag from the room. * \param userId * The id of the user to remove a tag for. The access token must be * authorized to make requests for this user ID. * \param roomId * The ID of the room to remove a tag from. * \param tag * The tag to remove. */ explicit DeleteRoomTagJob(const QString& userId, const QString& roomId, const QString& tag); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * DeleteRoomTagJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& tag); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/appservice_room_directory.h0000644000175000000620000000305513566674122025610 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Updates a room's visibility in the application service's room directory. /*! * Updates the visibility of a given room on the application service's room * directory. * * This API is similar to the room directory visibility API used by clients * to update the homeserver's more general room directory. * * This API requires the use of an application service access token * (``as_token``) instead of a typical client's access_token. This API cannot be * invoked by users who are not identified as application services. */ class UpdateAppserviceRoomDirectoryVsibilityJob : public BaseJob { public: /*! Updates a room's visibility in the application service's room directory. * \param networkId * The protocol (network) ID to update the room list for. This would * have been provided by the application service as being listed as * a supported protocol. * \param roomId * The room ID to add to the directory. * \param visibility * Whether the room should be visible (public) in the directory * or not (private). */ explicit UpdateAppserviceRoomDirectoryVsibilityJob(const QString& networkId, const QString& roomId, const QString& visibility); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/redaction.cpp0000644000175000000620000000223713566674122022633 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "redaction.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class RedactEventJob::Private { public: QString eventId; }; static const auto RedactEventJobName = QStringLiteral("RedactEventJob"); RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason) : BaseJob(HttpVerb::Put, RedactEventJobName, basePath % "/rooms/" % roomId % "/redact/" % eventId % "/" % txnId) , d(new Private) { QJsonObject _data; addParam(_data, QStringLiteral("reason"), reason); setRequestData(_data); } RedactEventJob::~RedactEventJob() = default; const QString& RedactEventJob::eventId() const { return d->eventId; } BaseJob::Status RedactEventJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("event_id"_ls), d->eventId); return Success; } spectral/include/libQuotient/lib/csapi/users.h0000644000175000000620000000521013566674122021463 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Searches the user directory. /*! * Performs a search for users on the homeserver. The homeserver may * determine which subset of users are searched, however the homeserver * MUST at a minimum consider the users the requesting user shares a * room with and those who reside in public rooms (known to the homeserver). * The search MUST consider local users to the homeserver, and SHOULD * query remote users as part of the search. * * The search is performed case-insensitively on user IDs and display * names preferably using a collation determined based upon the * ``Accept-Language`` header provided in the request, if present. */ class SearchUserDirectoryJob : public BaseJob { public: // Inner data structures /// Performs a search for users on the homeserver. The homeserver /// maydetermine which subset of users are searched, however the /// homeserverMUST at a minimum consider the users the requesting user /// shares aroom with and those who reside in public rooms (known to the /// homeserver).The search MUST consider local users to the homeserver, and /// SHOULDquery remote users as part of the search.The search is performed /// case-insensitively on user IDs and displaynames preferably using a /// collation determined based upon the ``Accept-Language`` header provided /// in the request, if present. struct User { /// The user's matrix user ID. QString userId; /// The display name of the user, if one exists. QString displayName; /// The avatar url, as an MXC, if one exists. QString avatarUrl; }; // Construction/destruction /*! Searches the user directory. * \param searchTerm * The term to search for * \param limit * The maximum number of results to return. Defaults to 10. */ explicit SearchUserDirectoryJob(const QString& searchTerm, Omittable limit = none); ~SearchUserDirectoryJob() override; // Result properties /// Ordered by rank and then whether or not profile info is available. const QVector& results() const; /// Indicates if the result list has been truncated by the limit. bool limited() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/third_party_lookup.h0000644000175000000620000001631513566674122024254 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/../application-service/definitions/location.h" #include "csapi/../application-service/definitions/protocol.h" #include "csapi/../application-service/definitions/user.h" #include "jobs/basejob.h" #include #include namespace Quotient { // Operations /// Retrieve metadata about all protocols that a homeserver supports. /*! * Fetches the overall metadata about protocols supported by the * homeserver. Includes both the available protocols and all fields * required for queries against each protocol. */ class GetProtocolsJob : public BaseJob { public: explicit GetProtocolsJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetProtocolsJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); ~GetProtocolsJob() override; // Result properties /// The protocols supported by the homeserver. const QHash& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Retrieve metadata about a specific protocol that the homeserver supports. /*! * Fetches the metadata from the homeserver about a particular third party * protocol. */ class GetProtocolMetadataJob : public BaseJob { public: /*! Retrieve metadata about a specific protocol that the homeserver * supports. \param protocol The name of the protocol. */ explicit GetProtocolMetadataJob(const QString& protocol); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetProtocolMetadataJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol); ~GetProtocolMetadataJob() override; // Result properties /// The protocol was found and metadata returned. const ThirdPartyProtocol& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Retrieve Matrix-side portals rooms leading to a third party location. /*! * Requesting this endpoint with a valid protocol name results in a list * of successful mapping results in a JSON array. Each result contains * objects to represent the Matrix room or rooms that represent a portal * to this third party network. Each has the Matrix room alias string, * an identifier for the particular third party network protocol, and an * object containing the network-specific fields that comprise this * identifier. It should attempt to canonicalise the identifier as much * as reasonably possible given the network type. */ class QueryLocationByProtocolJob : public BaseJob { public: /*! Retrieve Matrix-side portals rooms leading to a third party location. * \param protocol * The protocol used to communicate to the third party network. * \param searchFields * One or more custom fields to help identify the third party * location. */ explicit QueryLocationByProtocolJob(const QString& protocol, const QString& searchFields = {}); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * QueryLocationByProtocolJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol, const QString& searchFields = {}); ~QueryLocationByProtocolJob() override; // Result properties /// At least one portal room was found. const QVector& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Retrieve the Matrix User ID of a corresponding third party user. /*! * Retrieve a Matrix User ID linked to a user on the third party service, given * a set of user parameters. */ class QueryUserByProtocolJob : public BaseJob { public: /*! Retrieve the Matrix User ID of a corresponding third party user. * \param protocol * The name of the protocol. * \param fields * One or more custom fields that are passed to the AS to help identify * the user. */ explicit QueryUserByProtocolJob(const QString& protocol, const QString& fields = {}); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * QueryUserByProtocolJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& protocol, const QString& fields = {}); ~QueryUserByProtocolJob() override; // Result properties /// The Matrix User IDs found with the given parameters. const QVector& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Reverse-lookup third party locations given a Matrix room alias. /*! * Retrieve an array of third party network locations from a Matrix room * alias. */ class QueryLocationByAliasJob : public BaseJob { public: /*! Reverse-lookup third party locations given a Matrix room alias. * \param alias * The Matrix room alias to look up. */ explicit QueryLocationByAliasJob(const QString& alias); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * QueryLocationByAliasJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& alias); ~QueryLocationByAliasJob() override; // Result properties /// All found third party locations. const QVector& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Reverse-lookup third party users given a Matrix User ID. /*! * Retrieve an array of third party users from a Matrix User ID. */ class QueryUserByIDJob : public BaseJob { public: /*! Reverse-lookup third party users given a Matrix User ID. * \param userid * The Matrix User ID to look up. */ explicit QueryUserByIDJob(const QString& userid); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * QueryUserByIDJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userid); ~QueryUserByIDJob() override; // Result properties /// An array of third party users. const QVector& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/event_context.h0000644000175000000620000000423413566674122023214 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "events/eventloader.h" #include "jobs/basejob.h" namespace Quotient { // Operations /// Get events and state around the specified event. /*! * This API returns a number of events that happened just before and * after the specified event. This allows clients to get the context * surrounding an event. */ class GetEventContextJob : public BaseJob { public: /*! Get events and state around the specified event. * \param roomId * The room to get events from. * \param eventId * The event to get context around. * \param limit * The maximum number of events to return. Default: 10. */ explicit GetEventContextJob(const QString& roomId, const QString& eventId, Omittable limit = none); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetEventContextJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId, Omittable limit = none); ~GetEventContextJob() override; // Result properties /// A token that can be used to paginate backwards with. const QString& begin() const; /// A token that can be used to paginate forwards with. const QString& end() const; /// A list of room events that happened just before the /// requested event, in reverse-chronological order. RoomEvents&& eventsBefore(); /// Details of the requested event. RoomEventPtr&& event(); /// A list of room events that happened just after the /// requested event, in chronological order. RoomEvents&& eventsAfter(); /// The state of the room at the last event returned. StateEvents&& state(); protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/wellknown.h0000644000175000000620000000264113566674122022347 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/definitions/wellknown/full.h" #include "jobs/basejob.h" namespace Quotient { // Operations /// Gets Matrix server discovery information about the domain. /*! * Gets discovery information about the domain. The file may include * additional keys, which MUST follow the Java package naming convention, * e.g. ``com.example.myapp.property``. This ensures property names are * suitably namespaced for each application and reduces the risk of * clashes. * * Note that this endpoint is not necessarily handled by the homeserver, * but by another webserver, to be used for discovering the homeserver URL. */ class GetWellknownJob : public BaseJob { public: explicit GetWellknownJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetWellknownJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); ~GetWellknownJob() override; // Result properties /// Server discovery information. const DiscoveryInformation& data() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/versions.h0000644000175000000620000000451313566674122022177 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Gets the versions of the specification supported by the server. /*! * Gets the versions of the specification supported by the server. * * Values will take the form ``rX.Y.Z``. * * Only the latest ``Z`` value will be reported for each supported ``X.Y`` * value. i.e. if the server implements ``r0.0.0``, ``r0.0.1``, and ``r1.2.0``, * it will report ``r0.0.1`` and ``r1.2.0``. * * The server may additionally advertise experimental features it supports * through ``unstable_features``. These features should be namespaced and * may optionally include version information within their name if desired. * Features listed here are not for optionally toggling parts of the Matrix * specification and should only be used to advertise support for a feature * which has not yet landed in the spec. For example, a feature currently * undergoing the proposal process may appear here and eventually be taken * off this list once the feature lands in the spec and the server deems it * reasonable to do so. Servers may wish to keep advertising features here * after they've been released into the spec to give clients a chance to * upgrade appropriately. Additionally, clients should avoid using unstable * features in their stable releases. */ class GetVersionsJob : public BaseJob { public: explicit GetVersionsJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetVersionsJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); ~GetVersionsJob() override; // Result properties /// The supported versions. const QStringList& versions() const; /// Experimental features the server supports. Features not listed here, /// or the lack of this property all together, indicate that a feature is /// not supported. const QHash& unstableFeatures() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/room_send.h0000644000175000000620000000376513566674122022324 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Send a message event to the given room. /*! * This endpoint is used to send a message event to a room. Message events * allow access to historical events and pagination, making them suited * for "once-off" activity in a room. * * The body of the request should be the content object of the event; the * fields in this object will vary depending on the type of event. See * `Room Events`_ for the m. event specification. */ class SendMessageJob : public BaseJob { public: /*! Send a message event to the given room. * \param roomId * The room to send the event to. * \param eventType * The type of event to send. * \param txnId * The transaction ID for this event. Clients should generate an * ID unique across requests with the same access token; it will be * used by the server to ensure idempotency of requests. * \param body * This endpoint is used to send a message event to a room. Message events * allow access to historical events and pagination, making them suited * for "once-off" activity in a room. * * The body of the request should be the content object of the event; the * fields in this object will vary depending on the type of event. See * `Room Events`_ for the m. event specification. */ explicit SendMessageJob(const QString& roomId, const QString& eventType, const QString& txnId, const QJsonObject& body = {}); ~SendMessageJob() override; // Result properties /// A unique identifier for the event. const QString& eventId() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/{{base}}.h.mustache0000644000175000000620000000621213566674122024027 0ustar dilingerstaff{{>preamble}} #pragma once {{#operations}} #include "jobs/basejob.h"{{/operations}} {{#models}} #include "converters.h"{{/models}} {{#imports}} #include {{_}}{{/imports}} namespace Quotient { {{#models}} // Data structures {{# model}} {{#description}}/// {{_}}{{/description}} struct {{name}}{{#parents?}} : {{#parents}}{{name}}{{>cjoin}}{{/parents}}{{/parents?}} { {{# vars}}{{#description?}} /// {{#description}}{{_}}{{/description}}{{/description?}} {{>maybeOmittableType}} {{nameCamelCase}}; {{/ vars}} {{# propertyMap}} {{#description?}} /// {{#description}}{{_}}{{/description}}{{/description?}} {{>maybeOmittableType}} {{nameCamelCase}}; {{/ propertyMap}} }; template <> struct JsonObjectConverter<{{name}}> { {{#in?}}static void dumpTo(QJsonObject& jo, const {{name}}& pod);{{/in?}} {{#out?}}static void fillFrom({{>maybeCrefJsonObject}} jo, {{name}}& pod);{{/out?}}}; {{/ model}} {{/models}} {{#operations}}// Operations {{# operation}} {{#summary}}/// {{summary}}{{/summary}} {{#description?}}/*!{{#description}} * {{_}}{{/description}} */{{/description?}} class {{camelCaseOperationId}}Job : public BaseJob { public:{{#models}} // Inner data structures {{# model}} {{#description?}} /// {{#description}}{{_}}{{/description}}{{/description?}} struct {{name}}{{#parents?}} : {{#parents}}{{name}}{{>cjoin}}{{/parents}}{{/parents?}} { {{# vars}}{{#description?}} /// {{#description}}{{_}}{{/description}}{{/description?}} {{>maybeOmittableType}} {{nameCamelCase}}; {{/ vars}} {{# propertyMap}} {{#description?}} /// {{#description}}{{_}}{{/description}}{{/description?}} {{>maybeOmittableType}} {{nameCamelCase}}; {{/ propertyMap}} }; {{/ model}} // Construction/destruction {{/ models}}{{#allParams?}} /*! {{summary}}{{#allParams}} * \param {{nameCamelCase}}{{#description}} * {{_}}{{/description}}{{/allParams}} */{{/allParams?}} explicit {{camelCaseOperationId}}Job({{#allParams}}{{>joinedParamDecl}}{{/allParams}}); {{^ bodyParams}} /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * {{camelCaseOperationId}}Job is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl{{#allParams?}}, {{#allParams}}{{>joinedParamDecl}}{{/allParams}}{{/allParams?}}); {{/ bodyParams}} {{# responses}}{{#normalResponse?}}{{#allProperties?}} ~{{camelCaseOperationId}}Job() override; // Result properties {{#allProperties}}{{#description}} /// {{_}}{{/description}} {{>maybeCrefType}} {{paramName}}(){{^moveOnly}} const{{/moveOnly}};{{/allProperties}} protected:{{#producesNonJson?}} Status parseReply(QNetworkReply* reply) override; {{/producesNonJson?}}{{^producesNonJson?}} Status parseJson(const QJsonDocument& data) override; {{/producesNonJson?}} private: class Private; QScopedPointer d; {{/ allProperties?}}{{/normalResponse?}}{{/responses}} }; {{/ operation}} {{/operations}} } // namespace Quotient spectral/include/libQuotient/lib/csapi/sso_login_redirect.h0000644000175000000620000000211613566674122024201 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Redirect the user's browser to the SSO interface. /*! * A web-based Matrix client should instruct the user's browser to * navigate to this endpoint in order to log in via SSO. * * The server MUST respond with an HTTP redirect to the SSO interface. */ class RedirectToSSOJob : public BaseJob { public: /*! Redirect the user's browser to the SSO interface. * \param redirectUrl * URI to which the user will be redirected after the homeserver has * authenticated the user with SSO. */ explicit RedirectToSSOJob(const QString& redirectUrl); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * RedirectToSSOJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& redirectUrl); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/keys.cpp0000644000175000000620000001325313566674122021636 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "keys.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class UploadKeysJob::Private { public: QHash oneTimeKeyCounts; }; static const auto UploadKeysJobName = QStringLiteral("UploadKeysJob"); UploadKeysJob::UploadKeysJob(const Omittable& deviceKeys, const QHash& oneTimeKeys) : BaseJob(HttpVerb::Post, UploadKeysJobName, basePath % "/keys/upload") , d(new Private) { QJsonObject _data; addParam(_data, QStringLiteral("device_keys"), deviceKeys); addParam(_data, QStringLiteral("one_time_keys"), oneTimeKeys); setRequestData(_data); } UploadKeysJob::~UploadKeysJob() = default; const QHash& UploadKeysJob::oneTimeKeyCounts() const { return d->oneTimeKeyCounts; } BaseJob::Status UploadKeysJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("one_time_key_counts"_ls)) return { IncorrectResponse, "The key 'one_time_key_counts' not found in the response" }; fromJson(json.value("one_time_key_counts"_ls), d->oneTimeKeyCounts); return Success; } // Converters namespace Quotient { template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, QueryKeysJob::UnsignedDeviceInfo& result) { fromJson(jo.value("device_display_name"_ls), result.deviceDisplayName); } }; template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, QueryKeysJob::DeviceInformation& result) { fillFromJson(jo, result); fromJson(jo.value("unsigned"_ls), result.unsignedData); } }; } // namespace Quotient class QueryKeysJob::Private { public: QHash failures; QHash> deviceKeys; }; static const auto QueryKeysJobName = QStringLiteral("QueryKeysJob"); QueryKeysJob::QueryKeysJob(const QHash& deviceKeys, Omittable timeout, const QString& token) : BaseJob(HttpVerb::Post, QueryKeysJobName, basePath % "/keys/query") , d(new Private) { QJsonObject _data; addParam(_data, QStringLiteral("timeout"), timeout); addParam<>(_data, QStringLiteral("device_keys"), deviceKeys); addParam(_data, QStringLiteral("token"), token); setRequestData(_data); } QueryKeysJob::~QueryKeysJob() = default; const QHash& QueryKeysJob::failures() const { return d->failures; } const QHash>& QueryKeysJob::deviceKeys() const { return d->deviceKeys; } BaseJob::Status QueryKeysJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("failures"_ls), d->failures); fromJson(json.value("device_keys"_ls), d->deviceKeys); return Success; } class ClaimKeysJob::Private { public: QHash failures; QHash> oneTimeKeys; }; static const auto ClaimKeysJobName = QStringLiteral("ClaimKeysJob"); ClaimKeysJob::ClaimKeysJob( const QHash>& oneTimeKeys, Omittable timeout) : BaseJob(HttpVerb::Post, ClaimKeysJobName, basePath % "/keys/claim") , d(new Private) { QJsonObject _data; addParam(_data, QStringLiteral("timeout"), timeout); addParam<>(_data, QStringLiteral("one_time_keys"), oneTimeKeys); setRequestData(_data); } ClaimKeysJob::~ClaimKeysJob() = default; const QHash& ClaimKeysJob::failures() const { return d->failures; } const QHash>& ClaimKeysJob::oneTimeKeys() const { return d->oneTimeKeys; } BaseJob::Status ClaimKeysJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("failures"_ls), d->failures); fromJson(json.value("one_time_keys"_ls), d->oneTimeKeys); return Success; } class GetKeysChangesJob::Private { public: QStringList changed; QStringList left; }; BaseJob::Query queryToGetKeysChanges(const QString& from, const QString& to) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("from"), from); addParam<>(_q, QStringLiteral("to"), to); return _q; } QUrl GetKeysChangesJob::makeRequestUrl(QUrl baseUrl, const QString& from, const QString& to) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/keys/changes", queryToGetKeysChanges(from, to)); } static const auto GetKeysChangesJobName = QStringLiteral("GetKeysChangesJob"); GetKeysChangesJob::GetKeysChangesJob(const QString& from, const QString& to) : BaseJob(HttpVerb::Get, GetKeysChangesJobName, basePath % "/keys/changes", queryToGetKeysChanges(from, to)) , d(new Private) {} GetKeysChangesJob::~GetKeysChangesJob() = default; const QStringList& GetKeysChangesJob::changed() const { return d->changed; } const QStringList& GetKeysChangesJob::left() const { return d->left; } BaseJob::Status GetKeysChangesJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("changed"_ls), d->changed); fromJson(json.value("left"_ls), d->left); return Success; } spectral/include/libQuotient/lib/csapi/account-data.h0000644000175000000620000001022113566674122022663 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Set some account_data for the user. /*! * Set some account_data for the client. This config is only visible to the user * that set the account_data. The config will be synced to clients in the * top-level ``account_data``. */ class SetAccountDataJob : public BaseJob { public: /*! Set some account_data for the user. * \param userId * The ID of the user to set account_data for. The access token must be * authorized to make requests for this user ID. * \param type * The event type of the account_data to set. Custom types should be * namespaced to avoid clashes. * \param content * The content of the account_data */ explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {}); }; /// Get some account_data for the user. /*! * Get some account_data for the client. This config is only visible to the user * that set the account_data. */ class GetAccountDataJob : public BaseJob { public: /*! Get some account_data for the user. * \param userId * The ID of the user to get account_data for. The access token must be * authorized to make requests for this user ID. * \param type * The event type of the account_data to get. Custom types should be * namespaced to avoid clashes. */ explicit GetAccountDataJob(const QString& userId, const QString& type); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetAccountDataJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type); }; /// Set some account_data for the user. /*! * Set some account_data for the client on a given room. This config is only * visible to the user that set the account_data. The config will be synced to * clients in the per-room ``account_data``. */ class SetAccountDataPerRoomJob : public BaseJob { public: /*! Set some account_data for the user. * \param userId * The ID of the user to set account_data for. The access token must be * authorized to make requests for this user ID. * \param roomId * The ID of the room to set account_data on. * \param type * The event type of the account_data to set. Custom types should be * namespaced to avoid clashes. * \param content * The content of the account_data */ explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {}); }; /// Get some account_data for the user. /*! * Get some account_data for the client on a given room. This config is only * visible to the user that set the account_data. */ class GetAccountDataPerRoomJob : public BaseJob { public: /*! Get some account_data for the user. * \param userId * The ID of the user to set account_data for. The access token must be * authorized to make requests for this user ID. * \param roomId * The ID of the room to get account_data for. * \param type * The event type of the account_data to get. Custom types should be * namespaced to avoid clashes. */ explicit GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetAccountDataPerRoomJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId, const QString& type); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/login.cpp0000644000175000000620000000702313566674122021771 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "login.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); // Converters namespace Quotient { template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, GetLoginFlowsJob::LoginFlow& result) { fromJson(jo.value("type"_ls), result.type); } }; } // namespace Quotient class GetLoginFlowsJob::Private { public: QVector flows; }; QUrl GetLoginFlowsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/login"); } static const auto GetLoginFlowsJobName = QStringLiteral("GetLoginFlowsJob"); GetLoginFlowsJob::GetLoginFlowsJob() : BaseJob(HttpVerb::Get, GetLoginFlowsJobName, basePath % "/login", false) , d(new Private) {} GetLoginFlowsJob::~GetLoginFlowsJob() = default; const QVector& GetLoginFlowsJob::flows() const { return d->flows; } BaseJob::Status GetLoginFlowsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("flows"_ls), d->flows); return Success; } class LoginJob::Private { public: QString userId; QString accessToken; QString homeServer; QString deviceId; Omittable wellKnown; }; static const auto LoginJobName = QStringLiteral("LoginJob"); LoginJob::LoginJob(const QString& type, const Omittable& identifier, const QString& password, const QString& token, const QString& deviceId, const QString& initialDeviceDisplayName, const QString& user, const QString& medium, const QString& address) : BaseJob(HttpVerb::Post, LoginJobName, basePath % "/login", false) , d(new Private) { QJsonObject _data; addParam<>(_data, QStringLiteral("type"), type); addParam(_data, QStringLiteral("identifier"), identifier); addParam(_data, QStringLiteral("password"), password); addParam(_data, QStringLiteral("token"), token); addParam(_data, QStringLiteral("device_id"), deviceId); addParam(_data, QStringLiteral("initial_device_display_name"), initialDeviceDisplayName); addParam(_data, QStringLiteral("user"), user); addParam(_data, QStringLiteral("medium"), medium); addParam(_data, QStringLiteral("address"), address); setRequestData(_data); } LoginJob::~LoginJob() = default; const QString& LoginJob::userId() const { return d->userId; } const QString& LoginJob::accessToken() const { return d->accessToken; } const QString& LoginJob::homeServer() const { return d->homeServer; } const QString& LoginJob::deviceId() const { return d->deviceId; } const Omittable& LoginJob::wellKnown() const { return d->wellKnown; } BaseJob::Status LoginJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("user_id"_ls), d->userId); fromJson(json.value("access_token"_ls), d->accessToken); fromJson(json.value("home_server"_ls), d->homeServer); fromJson(json.value("device_id"_ls), d->deviceId); fromJson(json.value("well_known"_ls), d->wellKnown); return Success; } spectral/include/libQuotient/lib/csapi/leaving.h0000644000175000000620000000443013566674122021752 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Stop the requesting user participating in a particular room. /*! * This API stops a user participating in a particular room. * * If the user was already in the room, they will no longer be able to see * new events in the room. If the room requires an invite to join, they * will need to be re-invited before they can re-join. * * If the user was invited to the room, but had not joined, this call * serves to reject the invite. * * The user will still be allowed to retrieve history from the room which * they were previously allowed to see. */ class LeaveRoomJob : public BaseJob { public: /*! Stop the requesting user participating in a particular room. * \param roomId * The room identifier to leave. */ explicit LeaveRoomJob(const QString& roomId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * LeaveRoomJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); }; /// Stop the requesting user remembering about a particular room. /*! * This API stops a user remembering about a particular room. * * In general, history is a first class citizen in Matrix. After this API * is called, however, a user will no longer be able to retrieve history * for this room. If all users on a homeserver forget a room, the room is * eligible for deletion from that homeserver. * * If the user is currently joined to the room, they must leave the room * before calling this API. */ class ForgetRoomJob : public BaseJob { public: /*! Stop the requesting user remembering about a particular room. * \param roomId * The room identifier to forget. */ explicit ForgetRoomJob(const QString& roomId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * ForgetRoomJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/banning.h0000644000175000000620000000332413566674122021742 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Ban a user in the room. /*! * Ban a user in the room. If the user is currently in the room, also kick them. * * When a user is banned from a room, they may not join it or be invited to it * until they are unbanned. * * The caller must have the required power level in order to perform this * operation. */ class BanJob : public BaseJob { public: /*! Ban a user in the room. * \param roomId * The room identifier (not alias) from which the user should be banned. * \param userId * The fully qualified user ID of the user being banned. * \param reason * The reason the user has been banned. This will be supplied as the * ``reason`` on the target's updated `m.room.member`_ event. */ explicit BanJob(const QString& roomId, const QString& userId, const QString& reason = {}); }; /// Unban a user from the room. /*! * Unban a user from the room. This allows them to be invited to the room, * and join if they would otherwise be allowed to join according to its join * rules. * * The caller must have the required power level in order to perform this * operation. */ class UnbanJob : public BaseJob { public: /*! Unban a user from the room. * \param roomId * The room identifier (not alias) from which the user should be unbanned. * \param userId * The fully qualified user ID of the user being unbanned. */ explicit UnbanJob(const QString& roomId, const QString& userId); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/read_markers.cpp0000644000175000000620000000156113566674122023321 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "read_markers.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); static const auto SetReadMarkerJobName = QStringLiteral("SetReadMarkerJob"); SetReadMarkerJob::SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead) : BaseJob(HttpVerb::Post, SetReadMarkerJobName, basePath % "/rooms/" % roomId % "/read_markers") { QJsonObject _data; addParam<>(_data, QStringLiteral("m.fully_read"), mFullyRead); addParam(_data, QStringLiteral("m.read"), mRead); setRequestData(_data); } spectral/include/libQuotient/lib/csapi/content-repo.h0000644000175000000620000002203713566674122022745 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Upload some content to the content repository. class UploadContentJob : public BaseJob { public: /*! Upload some content to the content repository. * \param content * \param filename * The name of the file being uploaded * \param contentType * The content type of the file being uploaded */ explicit UploadContentJob(QIODevice* content, const QString& filename = {}, const QString& contentType = {}); ~UploadContentJob() override; // Result properties /// The MXC URI to the uploaded content. const QString& contentUri() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Download content from the content repository. class GetContentJob : public BaseJob { public: /*! Download content from the content repository. * \param serverName * The server name from the ``mxc://`` URI (the authoritory component) * \param mediaId * The media ID from the ``mxc://`` URI (the path component) * \param allowRemote * Indicates to the server that it should not attempt to fetch the media * if it is deemed remote. This is to prevent routing loops where the server * contacts itself. Defaults to true if not provided. */ explicit GetContentJob(const QString& serverName, const QString& mediaId, bool allowRemote = true); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetContentJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, bool allowRemote = true); ~GetContentJob() override; // Result properties /// The content type of the file that was previously uploaded. const QString& contentType() const; /// The name of the file that was previously uploaded, if set. const QString& contentDisposition() const; /// The content that was previously uploaded. QIODevice* data() const; protected: Status parseReply(QNetworkReply* reply) override; private: class Private; QScopedPointer d; }; /// Download content from the content repository as a given filename. class GetContentOverrideNameJob : public BaseJob { public: /*! Download content from the content repository as a given filename. * \param serverName * The server name from the ``mxc://`` URI (the authoritory component) * \param mediaId * The media ID from the ``mxc://`` URI (the path component) * \param fileName * The filename to give in the Content-Disposition * \param allowRemote * Indicates to the server that it should not attempt to fetch the media * if it is deemed remote. This is to prevent routing loops where the server * contacts itself. Defaults to true if not provided. */ explicit GetContentOverrideNameJob(const QString& serverName, const QString& mediaId, const QString& fileName, bool allowRemote = true); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetContentOverrideNameJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, const QString& fileName, bool allowRemote = true); ~GetContentOverrideNameJob() override; // Result properties /// The content type of the file that was previously uploaded. const QString& contentType() const; /// The name of file given in the request const QString& contentDisposition() const; /// The content that was previously uploaded. QIODevice* data() const; protected: Status parseReply(QNetworkReply* reply) override; private: class Private; QScopedPointer d; }; /// Download a thumbnail of the content from the content repository. class GetContentThumbnailJob : public BaseJob { public: /*! Download a thumbnail of the content from the content repository. * \param serverName * The server name from the ``mxc://`` URI (the authoritory component) * \param mediaId * The media ID from the ``mxc://`` URI (the path component) * \param width * The *desired* width of the thumbnail. The actual thumbnail may not * match the size specified. * \param height * The *desired* height of the thumbnail. The actual thumbnail may not * match the size specified. * \param method * The desired resizing method. * \param allowRemote * Indicates to the server that it should not attempt to fetch the media * if it is deemed remote. This is to prevent routing loops where the server * contacts itself. Defaults to true if not provided. */ explicit GetContentThumbnailJob(const QString& serverName, const QString& mediaId, int width, int height, const QString& method = {}, bool allowRemote = true); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetContentThumbnailJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, int width, int height, const QString& method = {}, bool allowRemote = true); ~GetContentThumbnailJob() override; // Result properties /// The content type of the thumbnail. const QString& contentType() const; /// A thumbnail of the requested content. QIODevice* data() const; protected: Status parseReply(QNetworkReply* reply) override; private: class Private; QScopedPointer d; }; /// Get information about a URL for a client class GetUrlPreviewJob : public BaseJob { public: /*! Get information about a URL for a client * \param url * The URL to get a preview of * \param ts * The preferred point in time to return a preview for. The server may * return a newer version if it does not have the requested version * available. */ explicit GetUrlPreviewJob(const QString& url, Omittable ts = none); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetUrlPreviewJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& url, Omittable ts = none); ~GetUrlPreviewJob() override; // Result properties /// The byte-size of the image. Omitted if there is no image attached. Omittable matrixImageSize() const; /// An MXC URI to the image. Omitted if there is no image. const QString& ogImage() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Get the configuration for the content repository. /*! * This endpoint allows clients to retrieve the configuration of the content * repository, such as upload limitations. * Clients SHOULD use this as a guide when using content repository endpoints. * All values are intentionally left optional. Clients SHOULD follow * the advice given in the field description when the field is not available. * * **NOTE:** Both clients and server administrators should be aware that proxies * between the client and the server may affect the apparent behaviour of * content repository APIs, for example, proxies may enforce a lower upload size * limit than is advertised by the server on this endpoint. */ class GetConfigJob : public BaseJob { public: explicit GetConfigJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetConfigJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); ~GetConfigJob() override; // Result properties /// The maximum size an upload can be in bytes. /// Clients SHOULD use this as a guide when uploading content. /// If not listed or null, the size limit should be treated as unknown. Omittable uploadSize() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/pusher.h0000644000175000000620000001410013566674122021626 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Gets the current pushers for the authenticated user /*! * Gets all currently active pushers for the authenticated user. */ class GetPushersJob : public BaseJob { public: // Inner data structures /// A dictionary of information for the pusher implementationitself. struct PusherData { /// Required if ``kind`` is ``http``. The URL to use to /// sendnotifications to. QString url; /// The format to use when sending notifications to the PushGateway. QString format; }; /// Gets all currently active pushers for the authenticated user. struct Pusher { /// This is a unique identifier for this pusher. See ``/set`` formore /// detail.Max length, 512 bytes. QString pushkey; /// The kind of pusher. ``"http"`` is a pusher thatsends HTTP pokes. QString kind; /// This is a reverse-DNS style identifier for the application.Max /// length, 64 chars. QString appId; /// A string that will allow the user to identify what applicationowns /// this pusher. QString appDisplayName; /// A string that will allow the user to identify what device ownsthis /// pusher. QString deviceDisplayName; /// This string determines which set of device specific rules thispusher /// executes. QString profileTag; /// The preferred language for receiving notifications (e.g. 'en'or /// 'en-US') QString lang; /// A dictionary of information for the pusher implementationitself. PusherData data; }; // Construction/destruction explicit GetPushersJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetPushersJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); ~GetPushersJob() override; // Result properties /// An array containing the current pushers for the user const QVector& pushers() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Modify a pusher for this user on the homeserver. /*! * This endpoint allows the creation, modification and deletion of `pushers`_ * for this user ID. The behaviour of this endpoint varies depending on the * values in the JSON body. */ class PostPusherJob : public BaseJob { public: // Inner data structures /// A dictionary of information for the pusher implementationitself. If /// ``kind`` is ``http``, this should contain ``url``which is the URL to use /// to send notifications to. struct PusherData { /// Required if ``kind`` is ``http``. The URL to use to sendnotifications /// to. MUST be an HTTPS URL with a path of ``/_matrix/push/v1/notify``. QString url; /// The format to send notifications in to Push Gateways if the``kind`` /// is ``http``. The details about what fields thehomeserver should send /// to the push gateway are defined in the`Push Gateway Specification`_. /// Currently the only formatavailable is 'event_id_only'. QString format; }; // Construction/destruction /*! Modify a pusher for this user on the homeserver. * \param pushkey * This is a unique identifier for this pusher. The value you * should use for this is the routing or destination address * information for the notification, for example, the APNS token * for APNS or the Registration ID for GCM. If your notification * client has no such concept, use any unique identifier. * Max length, 512 bytes. * * If the ``kind`` is ``"email"``, this is the email address to * send notifications to. * \param kind * The kind of pusher to configure. ``"http"`` makes a pusher that * sends HTTP pokes. ``"email"`` makes a pusher that emails the * user with unread notifications. ``null`` deletes the pusher. * \param appId * This is a reverse-DNS style identifier for the application. * It is recommended that this end with the platform, such that * different platform versions get different app identifiers. * Max length, 64 chars. * * If the ``kind`` is ``"email"``, this is ``"m.email"``. * \param appDisplayName * A string that will allow the user to identify what application * owns this pusher. * \param deviceDisplayName * A string that will allow the user to identify what device owns * this pusher. * \param lang * The preferred language for receiving notifications (e.g. 'en' * or 'en-US'). * \param data * A dictionary of information for the pusher implementation * itself. If ``kind`` is ``http``, this should contain ``url`` * which is the URL to use to send notifications to. * \param profileTag * This string determines which set of device specific rules this * pusher executes. * \param append * If true, the homeserver should add another pusher with the * given pushkey and App ID in addition to any others with * different user IDs. Otherwise, the homeserver must remove any * other pushers with the same App ID and pushkey for different * users. The default is ``false``. */ explicit PostPusherJob(const QString& pushkey, const QString& kind, const QString& appId, const QString& appDisplayName, const QString& deviceDisplayName, const QString& lang, const PusherData& data, const QString& profileTag = {}, Omittable append = none); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/keys.h0000644000175000000620000001724513566674122021310 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/definitions/device_keys.h" #include "jobs/basejob.h" #include #include #include namespace Quotient { // Operations /// Upload end-to-end encryption keys. /*! * Publishes end-to-end encryption keys for the device. */ class UploadKeysJob : public BaseJob { public: /*! Upload end-to-end encryption keys. * \param deviceKeys * Identity keys for the device. May be absent if no new * identity keys are required. * \param oneTimeKeys * One-time public keys for "pre-key" messages. The names of * the properties should be in the format * ``:``. The format of the key is determined * by the key algorithm. * * May be absent if no new one-time keys are required. */ explicit UploadKeysJob(const Omittable& deviceKeys = none, const QHash& oneTimeKeys = {}); ~UploadKeysJob() override; // Result properties /// For each key algorithm, the number of unclaimed one-time keys /// of that type currently held on the server for this device. const QHash& oneTimeKeyCounts() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Download device identity keys. /*! * Returns the current devices and identity keys for the given users. */ class QueryKeysJob : public BaseJob { public: // Inner data structures /// Additional data added to the device key informationby intermediate /// servers, and not covered by thesignatures. struct UnsignedDeviceInfo { /// The display name which the user set on the device. QString deviceDisplayName; }; /// Returns the current devices and identity keys for the given users. struct DeviceInformation : DeviceKeys { /// Additional data added to the device key informationby intermediate /// servers, and not covered by thesignatures. Omittable unsignedData; }; // Construction/destruction /*! Download device identity keys. * \param deviceKeys * The keys to be downloaded. A map from user ID, to a list of * device IDs, or to an empty list to indicate all devices for the * corresponding user. * \param timeout * The time (in milliseconds) to wait when downloading keys from * remote servers. 10 seconds is the recommended default. * \param token * If the client is fetching keys as a result of a device update received * in a sync request, this should be the 'since' token of that sync * request, or any later sync token. This allows the server to ensure its * response contains the keys advertised by the notification in that sync. */ explicit QueryKeysJob(const QHash& deviceKeys, Omittable timeout = none, const QString& token = {}); ~QueryKeysJob() override; // Result properties /// If any remote homeservers could not be reached, they are /// recorded here. The names of the properties are the names of /// the unreachable servers. /// /// If the homeserver could be reached, but the user or device /// was unknown, no failure is recorded. Instead, the corresponding /// user or device is missing from the ``device_keys`` result. const QHash& failures() const; /// Information on the queried devices. A map from user ID, to a /// map from device ID to device information. For each device, /// the information returned will be the same as uploaded via /// ``/keys/upload``, with the addition of an ``unsigned`` /// property. const QHash>& deviceKeys() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Claim one-time encryption keys. /*! * Claims one-time keys for use in pre-key messages. */ class ClaimKeysJob : public BaseJob { public: /*! Claim one-time encryption keys. * \param oneTimeKeys * The keys to be claimed. A map from user ID, to a map from * device ID to algorithm name. * \param timeout * The time (in milliseconds) to wait when downloading keys from * remote servers. 10 seconds is the recommended default. */ explicit ClaimKeysJob( const QHash>& oneTimeKeys, Omittable timeout = none); ~ClaimKeysJob() override; // Result properties /// If any remote homeservers could not be reached, they are /// recorded here. The names of the properties are the names of /// the unreachable servers. /// /// If the homeserver could be reached, but the user or device /// was unknown, no failure is recorded. Instead, the corresponding /// user or device is missing from the ``one_time_keys`` result. const QHash& failures() const; /// One-time keys for the queried devices. A map from user ID, to a /// map from devices to a map from ``:`` to the key object. const QHash>& oneTimeKeys() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; /// Query users with recent device key updates. /*! * Gets a list of users who have updated their device identity keys since a * previous sync token. * * The server should include in the results any users who: * * * currently share a room with the calling user (ie, both users have * membership state ``join``); *and* * * added new device identity keys or removed an existing device with * identity keys, between ``from`` and ``to``. */ class GetKeysChangesJob : public BaseJob { public: /*! Query users with recent device key updates. * \param from * The desired start point of the list. Should be the ``next_batch`` field * from a response to an earlier call to |/sync|. Users who have not * uploaded new device identity keys since this point, nor deleted * existing devices with identity keys since then, will be excluded * from the results. * \param to * The desired end point of the list. Should be the ``next_batch`` * field from a recent call to |/sync| - typically the most recent * such call. This may be used by the server as a hint to check its * caches are up to date. */ explicit GetKeysChangesJob(const QString& from, const QString& to); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetKeysChangesJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& from, const QString& to); ~GetKeysChangesJob() override; // Result properties /// The Matrix User IDs of all users who updated their device /// identity keys. const QStringList& changed() const; /// The Matrix User IDs of all users who may have left all /// the end-to-end encrypted rooms they previously shared /// with the user. const QStringList& left() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/logout.h0000644000175000000620000000312013566674122021631 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "jobs/basejob.h" namespace Quotient { // Operations /// Invalidates a user access token /*! * Invalidates an existing access token, so that it can no longer be used for * authorization. */ class LogoutJob : public BaseJob { public: explicit LogoutJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * LogoutJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); }; /// Invalidates all access tokens for a user /*! * Invalidates all access tokens for a user, so that they can no longer be used * for authorization. This includes the access token that made this request. * * This endpoint does not require UI authorization because UI authorization is * designed to protect against attacks where the someone gets hold of a single * access token then takes over the account. This endpoint invalidates all * access tokens for the user, including the token used in the request, and * therefore the attacker is unable to take over the account in this way. */ class LogoutAllJob : public BaseJob { public: explicit LogoutAllJob(); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * LogoutAllJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/receipts.cpp0000644000175000000620000000143213566674122022475 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "receipts.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); static const auto PostReceiptJobName = QStringLiteral("PostReceiptJob"); PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType, const QString& eventId, const QJsonObject& receipt) : BaseJob(HttpVerb::Post, PostReceiptJobName, basePath % "/rooms/" % roomId % "/receipt/" % receiptType % "/" % eventId) { setRequestData(Data(toJson(receipt))); } spectral/include/libQuotient/lib/csapi/wellknown.cpp0000644000175000000620000000203513566674122022677 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "wellknown.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/.well-known"); class GetWellknownJob::Private { public: DiscoveryInformation data; }; QUrl GetWellknownJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/matrix/client"); } static const auto GetWellknownJobName = QStringLiteral("GetWellknownJob"); GetWellknownJob::GetWellknownJob() : BaseJob(HttpVerb::Get, GetWellknownJobName, basePath % "/matrix/client", false) , d(new Private) {} GetWellknownJob::~GetWellknownJob() = default; const DiscoveryInformation& GetWellknownJob::data() const { return d->data; } BaseJob::Status GetWellknownJob::parseJson(const QJsonDocument& data) { fromJson(data, d->data); return Success; } spectral/include/libQuotient/lib/csapi/versions.cpp0000644000175000000620000000256613566674122022540 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "versions.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client"); class GetVersionsJob::Private { public: QStringList versions; QHash unstableFeatures; }; QUrl GetVersionsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/versions"); } static const auto GetVersionsJobName = QStringLiteral("GetVersionsJob"); GetVersionsJob::GetVersionsJob() : BaseJob(HttpVerb::Get, GetVersionsJobName, basePath % "/versions", false) , d(new Private) {} GetVersionsJob::~GetVersionsJob() = default; const QStringList& GetVersionsJob::versions() const { return d->versions; } const QHash& GetVersionsJob::unstableFeatures() const { return d->unstableFeatures; } BaseJob::Status GetVersionsJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("versions"_ls)) return { IncorrectResponse, "The key 'versions' not found in the response" }; fromJson(json.value("versions"_ls), d->versions); fromJson(json.value("unstable_features"_ls), d->unstableFeatures); return Success; } spectral/include/libQuotient/lib/csapi/capabilities.cpp0000644000175000000620000000455713566674122023323 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "capabilities.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); // Converters namespace Quotient { template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::ChangePasswordCapability& result) { fromJson(jo.value("enabled"_ls), result.enabled); } }; template <> struct JsonObjectConverter { static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::RoomVersionsCapability& result) { fromJson(jo.value("default"_ls), result.defaultVersion); fromJson(jo.value("available"_ls), result.available); } }; template <> struct JsonObjectConverter { static void fillFrom(QJsonObject jo, GetCapabilitiesJob::Capabilities& result) { fromJson(jo.take("m.change_password"_ls), result.changePassword); fromJson(jo.take("m.room_versions"_ls), result.roomVersions); fromJson(jo, result.additionalProperties); } }; } // namespace Quotient class GetCapabilitiesJob::Private { public: Capabilities capabilities; }; QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/capabilities"); } static const auto GetCapabilitiesJobName = QStringLiteral("GetCapabilitiesJob"); GetCapabilitiesJob::GetCapabilitiesJob() : BaseJob(HttpVerb::Get, GetCapabilitiesJobName, basePath % "/capabilities") , d(new Private) {} GetCapabilitiesJob::~GetCapabilitiesJob() = default; const GetCapabilitiesJob::Capabilities& GetCapabilitiesJob::capabilities() const { return d->capabilities; } BaseJob::Status GetCapabilitiesJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("capabilities"_ls)) return { IncorrectResponse, "The key 'capabilities' not found in the response" }; fromJson(json.value("capabilities"_ls), d->capabilities); return Success; } spectral/include/libQuotient/lib/csapi/gtad.yaml0000644000175000000620000001330413566674122021757 0ustar dilingerstaffanalyzer: subst: "%CLIENT_RELEASE_LABEL%": r0 "%CLIENT_MAJOR_VERSION%": r0 identifiers: signed: signedData unsigned: unsignedData PushRule/default: isDefault default: defaultVersion # getCapabilities/RoomVersionsCapability origin_server_ts: originServerTimestamp # Instead of originServerTs start: begin # Because start() is a method in BaseJob m.upload.size: uploadSize m.homeserver: homeserver m.identity_server: identityServer m.change_password: changePassword m.room_versions: roomVersions AuthenticationData/additionalProperties: authInfo # Structure inside `types`: # - swaggerType: # OR # - swaggerType: # - swaggerFormat: # - /swaggerFormatRegEx/: # - //: # default, if the format doesn't mach anything above # WHERE # targetTypeSpec = targetType OR # { type: targetType, imports: , } # swaggerType can be +set/+on pair; attributes from the map under +set # are added to each type from the sequence under +on. types: - +set: &UseOmittable useOmittable: imports: [ '"converters.h"' ] omittedValue: 'none' # See `none` in converters.h +on: - integer: - int64: qint64 - int32: qint32 - //: int - number: - float: float - //: double - boolean: bool - string: - byte: &ByteStream type: QIODevice* imports: - binary: *ByteStream - +set: { avoidCopy: } +on: - date: type: QDate initializer: QDate::fromString("{{defaultValue}}") imports: - dateTime: type: QDateTime initializer: QDateTime::fromString("{{defaultValue}}") imports: - //: &QString type: QString initializer: QStringLiteral("{{defaultValue}}") isString: - file: *ByteStream - +set: { avoidCopy: } +on: - object: &QJsonObject { type: QJsonObject, imports: } - $ref: - +set: { moveOnly: } +on: - /state_event.yaml$/: { type: StateEventPtr, imports: '"events/eventloader.h"' } - /room_event.yaml$/: { type: RoomEventPtr, imports: '"events/eventloader.h"' } - /event.yaml$/: { type: EventPtr, imports: '"events/eventloader.h"' } - /m\.room\.member$/: pass # This $ref is only used in an array, see below - //: *UseOmittable # Also apply "avoidCopy" to all other ref'ed types - schema: # Properties of inline structure definitions - TurnServerCredentials: *QJsonObject # Because it's used as is - //: *UseOmittable - array: - string: QStringList - +set: { moveOnly: } +on: - /^Notification|Result$/: type: "std::vector<{{1}}>" imports: '"events/eventloader.h"' - /m\.room\.member$/: type: "EventsArray" imports: '"events/roommemberevent.h"' - /state_event.yaml$/: StateEvents - /room_event.yaml$/: RoomEvents - /event.yaml$/: Events - //: { type: "QVector<{{1}}>", imports: } - map: # `additionalProperties` in OpenAPI - RoomState: type: "std::unordered_map" moveOnly: imports: - /.+/: type: "QHash" imports: - //: type: QVariantHash imports: - variant: # A sequence `type` (multitype) in OpenAPI - /^string,null|null,string$/: *QString - //: { type: QVariant, imports: } #operations: mustache: constants: # Syntax elements used by GTAD # _quote: '"' # Common quote for left and right # _leftQuote: '"' # _rightQuote: '"' # _joinChar: ',' # The character used by {{_join}} - not working yet _comment: '//' copyrightName: Kitsune Ral copyrightEmail: partials: _typeRenderer: "{{#scope}}{{scopeCamelCase}}Job::{{/scope}}{{>name}}" omittedValue: '{}' # default value to initialize omitted parameters with initializer: '{{defaultValue}}' cjoin: '{{#hasMore}}, {{/hasMore}}' openOmittable: "{{^required?}}{{#useOmittable}}{{^defaultValue}}Omittable<{{/defaultValue}}{{/useOmittable}}{{/required?}}" closeOmittable: "{{^required?}}{{#useOmittable}}{{^defaultValue}}>{{/defaultValue}}{{/useOmittable}}{{/required?}}" maybeOmittableType: "{{>openOmittable}}{{dataType.name}}{{>closeOmittable}}" qualifiedMaybeOmittableType: "{{>openOmittable}}{{dataType.qualifiedName}}{{>closeOmittable}}" maybeCrefType: "{{#avoidCopy}}const {{/avoidCopy}}{{>maybeOmittableType}}{{#avoidCopy}}&{{/avoidCopy}}{{#moveOnly}}&&{{/moveOnly}}" qualifiedMaybeCrefType: "{{#avoidCopy}}const {{/avoidCopy}}{{>qualifiedMaybeOmittableType}}{{#avoidCopy}}&{{/avoidCopy}}{{#moveOnly}}&&{{/moveOnly}}" maybeCrefJsonObject: "{{^propertyMap}}const QJsonObject&{{/propertyMap}}{{#propertyMap}}QJsonObject{{/propertyMap}}" takeOrValue: "{{#propertyMap}}take{{/propertyMap}}{{^propertyMap}}value{{/propertyMap}}" initializeDefaultValue: "{{#defaultValue}}{{>initializer}}{{/defaultValue}}{{^defaultValue}}{{>omittedValue}}{{/defaultValue}}" joinedParamDecl: '{{>maybeCrefType}} {{paramName}}{{^required?}} = {{>initializeDefaultValue}}{{/required?}}{{>cjoin}}' joinedParamDef: '{{>maybeCrefType}} {{paramName}}{{>cjoin}}' passQueryParams: '{{#queryParams}}{{paramName}}{{>cjoin}}{{/queryParams}}' templates: - "{{base}}.h.mustache" - "{{base}}.cpp.mustache" #outFilesList: apifiles.txt spectral/include/libQuotient/lib/csapi/sso_login_redirect.cpp0000644000175000000620000000207413566674122024537 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "sso_login_redirect.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); BaseJob::Query queryToRedirectToSSO(const QString& redirectUrl) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("redirectUrl"), redirectUrl); return _q; } QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/login/sso/redirect", queryToRedirectToSSO(redirectUrl)); } static const auto RedirectToSSOJobName = QStringLiteral("RedirectToSSOJob"); RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl) : BaseJob(HttpVerb::Get, RedirectToSSOJobName, basePath % "/login/sso/redirect", queryToRedirectToSSO(redirectUrl), {}, false) {} spectral/include/libQuotient/lib/csapi/presence.h0000644000175000000620000000416713566674122022140 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "jobs/basejob.h" namespace Quotient { // Operations /// Update this user's presence state. /*! * This API sets the given user's presence state. When setting the status, * the activity time is updated to reflect that activity; the client does * not need to specify the ``last_active_ago`` field. You cannot set the * presence state of another user. */ class SetPresenceJob : public BaseJob { public: /*! Update this user's presence state. * \param userId * The user whose presence state to update. * \param presence * The new presence state. * \param statusMsg * The status message to attach to this state. */ explicit SetPresenceJob(const QString& userId, const QString& presence, const QString& statusMsg = {}); }; /// Get this user's presence state. /*! * Get the given user's presence state. */ class GetPresenceJob : public BaseJob { public: /*! Get this user's presence state. * \param userId * The user whose presence state to get. */ explicit GetPresenceJob(const QString& userId); /*! Construct a URL without creating a full-fledged job object * * This function can be used when a URL for * GetPresenceJob is necessary but the job * itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& userId); ~GetPresenceJob() override; // Result properties /// This user's presence. const QString& presence() const; /// The length of time in milliseconds since an action was performed /// by this user. Omittable lastActiveAgo() const; /// The state message for this user if one was set. const QString& statusMsg() const; /// Whether the user is currently active Omittable currentlyActive() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/room_send.cpp0000644000175000000620000000212613566674122022645 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "room_send.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); class SendMessageJob::Private { public: QString eventId; }; static const auto SendMessageJobName = QStringLiteral("SendMessageJob"); SendMessageJob::SendMessageJob(const QString& roomId, const QString& eventType, const QString& txnId, const QJsonObject& body) : BaseJob(HttpVerb::Put, SendMessageJobName, basePath % "/rooms/" % roomId % "/send/" % eventType % "/" % txnId) , d(new Private) { setRequestData(Data(toJson(body))); } SendMessageJob::~SendMessageJob() = default; const QString& SendMessageJob::eventId() const { return d->eventId; } BaseJob::Status SendMessageJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("event_id"_ls), d->eventId); return Success; } spectral/include/libQuotient/lib/csapi/definitions/0002755000175000000620000000000013566674122022470 5ustar dilingerstaffspectral/include/libQuotient/lib/csapi/definitions/auth_data.cpp0000644000175000000620000000136213566674122025126 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "auth_data.h" using namespace Quotient; void JsonObjectConverter::dumpTo( QJsonObject& jo, const AuthenticationData& pod) { fillJson(jo, pod.authInfo); addParam<>(jo, QStringLiteral("type"), pod.type); addParam(jo, QStringLiteral("session"), pod.session); } void JsonObjectConverter::fillFrom(QJsonObject jo, AuthenticationData& result) { fromJson(jo.take("type"_ls), result.type); fromJson(jo.take("session"_ls), result.session); fromJson(jo, result.authInfo); } spectral/include/libQuotient/lib/csapi/definitions/public_rooms_response.h0000644000175000000620000000413113566674122027251 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include namespace Quotient { // Data structures struct PublicRoomsChunk { /// Aliases of the room. May be empty. QStringList aliases; /// The canonical alias of the room, if any. QString canonicalAlias; /// The name of the room, if any. QString name; /// The number of members joined to the room. int numJoinedMembers; /// The ID of the room. QString roomId; /// The topic of the room, if any. QString topic; /// Whether the room may be viewed by guest users without joining. bool worldReadable; /// Whether guest users may join the room and participate in it.If they can, /// they will be subject to ordinary power levelrules like any other user. bool guestCanJoin; /// The URL for the room's avatar, if one is set. QString avatarUrl; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod); static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod); }; /// A list of the rooms on the server. struct PublicRoomsResponse { /// A paginated chunk of public rooms. QVector chunk; /// A pagination token for the response. The absence of this tokenmeans /// there are no more results to fetch and the client shouldstop paginating. QString nextBatch; /// A pagination token that allows fetching previous results. Theabsence of /// this token means there are no results before thisbatch, i.e. this is the /// first batch. QString prevBatch; /// An estimate on the total number of public rooms, if theserver has an /// estimate. Omittable totalRoomCountEstimate; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const PublicRoomsResponse& pod); static void fillFrom(const QJsonObject& jo, PublicRoomsResponse& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/definitions/push_rule.h0000644000175000000620000000233613566674122024651 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/definitions/push_condition.h" #include #include #include namespace Quotient { // Data structures struct PushRule { /// The actions to perform when this rule is matched. QVector actions; /// Whether this is a default rule, or has been set explicitly. bool isDefault; /// Whether the push rule is enabled or not. bool enabled; /// The ID of this rule. QString ruleId; /// The conditions that must hold true for an event in order for a rule to /// beapplied to an event. A rule with no conditions always matches. /// Onlyapplicable to ``underride`` and ``override`` rules. QVector conditions; /// The glob-style pattern to match against. Only applicable to /// ``content``rules. QString pattern; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const PushRule& pod); static void fillFrom(const QJsonObject& jo, PushRule& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/definitions/public_rooms_response.cpp0000644000175000000620000000500013566674122027600 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "public_rooms_response.h" using namespace Quotient; void JsonObjectConverter::dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod) { addParam(jo, QStringLiteral("aliases"), pod.aliases); addParam(jo, QStringLiteral("canonical_alias"), pod.canonicalAlias); addParam(jo, QStringLiteral("name"), pod.name); addParam<>(jo, QStringLiteral("num_joined_members"), pod.numJoinedMembers); addParam<>(jo, QStringLiteral("room_id"), pod.roomId); addParam(jo, QStringLiteral("topic"), pod.topic); addParam<>(jo, QStringLiteral("world_readable"), pod.worldReadable); addParam<>(jo, QStringLiteral("guest_can_join"), pod.guestCanJoin); addParam(jo, QStringLiteral("avatar_url"), pod.avatarUrl); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, PublicRoomsChunk& result) { fromJson(jo.value("aliases"_ls), result.aliases); fromJson(jo.value("canonical_alias"_ls), result.canonicalAlias); fromJson(jo.value("name"_ls), result.name); fromJson(jo.value("num_joined_members"_ls), result.numJoinedMembers); fromJson(jo.value("room_id"_ls), result.roomId); fromJson(jo.value("topic"_ls), result.topic); fromJson(jo.value("world_readable"_ls), result.worldReadable); fromJson(jo.value("guest_can_join"_ls), result.guestCanJoin); fromJson(jo.value("avatar_url"_ls), result.avatarUrl); } void JsonObjectConverter::dumpTo( QJsonObject& jo, const PublicRoomsResponse& pod) { addParam<>(jo, QStringLiteral("chunk"), pod.chunk); addParam(jo, QStringLiteral("next_batch"), pod.nextBatch); addParam(jo, QStringLiteral("prev_batch"), pod.prevBatch); addParam(jo, QStringLiteral("total_room_count_estimate"), pod.totalRoomCountEstimate); } void JsonObjectConverter::fillFrom( const QJsonObject& jo, PublicRoomsResponse& result) { fromJson(jo.value("chunk"_ls), result.chunk); fromJson(jo.value("next_batch"_ls), result.nextBatch); fromJson(jo.value("prev_batch"_ls), result.prevBatch); fromJson(jo.value("total_room_count_estimate"_ls), result.totalRoomCountEstimate); } spectral/include/libQuotient/lib/csapi/definitions/push_ruleset.h0000644000175000000620000000126313566674122025363 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/definitions/push_rule.h" #include namespace Quotient { // Data structures struct PushRuleset { QVector content; QVector override; QVector room; QVector sender; QVector underride; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const PushRuleset& pod); static void fillFrom(const QJsonObject& jo, PushRuleset& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/definitions/room_event_filter.cpp0000644000175000000620000000171713566674122026722 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "room_event_filter.h" using namespace Quotient; void JsonObjectConverter::dumpTo(QJsonObject& jo, const RoomEventFilter& pod) { fillJson(jo, pod); addParam(jo, QStringLiteral("not_rooms"), pod.notRooms); addParam(jo, QStringLiteral("rooms"), pod.rooms); addParam(jo, QStringLiteral("contains_url"), pod.containsUrl); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, RoomEventFilter& result) { fillFromJson(jo, result); fromJson(jo.value("not_rooms"_ls), result.notRooms); fromJson(jo.value("rooms"_ls), result.rooms); fromJson(jo.value("contains_url"_ls), result.containsUrl); } spectral/include/libQuotient/lib/csapi/definitions/sync_filter.h0000644000175000000620000001052113566674122025157 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/definitions/event_filter.h" #include "csapi/definitions/room_event_filter.h" namespace Quotient { // Data structures /// The state events to include for rooms. struct StateFilter : RoomEventFilter { /// If ``true``, the only ``m.room.member`` events returned inthe ``state`` /// section of the ``/sync`` response are thosewhich are definitely necessary /// for a client to displaythe ``sender`` of the timeline events in that /// response.If ``false``, ``m.room.member`` events are not filtered.By /// default, servers should suppress duplicate redundantlazy-loaded /// ``m.room.member`` events from being sent to a givenclient across multiple /// calls to ``/sync``, given that most clientscache membership events (see /// ``include_redundant_members``to change this behaviour). Omittable lazyLoadMembers; /// If ``true``, the ``state`` section of the ``/sync`` response willalways /// contain the ``m.room.member`` events required to displaythe ``sender`` /// of the timeline events in that response, assuming``lazy_load_members`` /// is enabled. This means that redundantduplicate member events may be /// returned across multiple calls to``/sync``. This is useful for naive /// clients who never trackmembership data. If ``false``, duplicate /// ``m.room.member`` eventsmay be suppressed by the server across multiple /// calls to ``/sync``.If ``lazy_load_members`` is ``false`` this field is /// ignored. Omittable includeRedundantMembers; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const StateFilter& pod); static void fillFrom(const QJsonObject& jo, StateFilter& pod); }; /// Filters to be applied to room data. struct RoomFilter { /// A list of room IDs to exclude. If this list is absent then no rooms are /// excluded. A matching room will be excluded even if it is listed in the /// ``'rooms'`` filter. This filter is applied before the filters in /// ``ephemeral``, ``state``, ``timeline`` or ``account_data`` QStringList notRooms; /// A list of room IDs to include. If this list is absent then all rooms are /// included. This filter is applied before the filters in ``ephemeral``, /// ``state``, ``timeline`` or ``account_data`` QStringList rooms; /// The events that aren't recorded in the room history, e.g. typing and /// receipts, to include for rooms. Omittable ephemeral; /// Include rooms that the user has left in the sync, default false Omittable includeLeave; /// The state events to include for rooms. Omittable state; /// The message and state update events to include for rooms. Omittable timeline; /// The per user account data to include for rooms. Omittable accountData; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const RoomFilter& pod); static void fillFrom(const QJsonObject& jo, RoomFilter& pod); }; struct Filter { /// List of event fields to include. If this list is absent then all fields /// are included. The entries may include '.' charaters to indicate /// sub-fields. So ['content.body'] will include the 'body' field of the /// 'content' object. A literal '.' character in a field name may be escaped /// using a '\\'. A server may include more fields than were requested. QStringList eventFields; /// The format to use for events. 'client' will return the events in a /// format suitable for clients. 'federation' will return the raw event as /// receieved over federation. The default is 'client'. QString eventFormat; /// The presence updates to include. Omittable presence; /// The user account data that isn't associated with rooms to include. Omittable accountData; /// Filters to be applied to room data. Omittable room; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const Filter& pod); static void fillFrom(const QJsonObject& jo, Filter& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/definitions/event_filter.cpp0000644000175000000620000000211713566674122025661 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "event_filter.h" using namespace Quotient; void JsonObjectConverter::dumpTo(QJsonObject& jo, const EventFilter& pod) { addParam(jo, QStringLiteral("limit"), pod.limit); addParam(jo, QStringLiteral("not_senders"), pod.notSenders); addParam(jo, QStringLiteral("not_types"), pod.notTypes); addParam(jo, QStringLiteral("senders"), pod.senders); addParam(jo, QStringLiteral("types"), pod.types); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, EventFilter& result) { fromJson(jo.value("limit"_ls), result.limit); fromJson(jo.value("not_senders"_ls), result.notSenders); fromJson(jo.value("not_types"_ls), result.notTypes); fromJson(jo.value("senders"_ls), result.senders); fromJson(jo.value("types"_ls), result.types); } spectral/include/libQuotient/lib/csapi/definitions/wellknown/0002755000175000000620000000000013566674122024510 5ustar dilingerstaffspectral/include/libQuotient/lib/csapi/definitions/wellknown/identity_server.cpp0000644000175000000620000000107213566674122030431 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "identity_server.h" using namespace Quotient; void JsonObjectConverter::dumpTo( QJsonObject& jo, const IdentityServerInformation& pod) { addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); } void JsonObjectConverter::fillFrom( const QJsonObject& jo, IdentityServerInformation& result) { fromJson(jo.value("base_url"_ls), result.baseUrl); } spectral/include/libQuotient/lib/csapi/definitions/wellknown/homeserver.h0000644000175000000620000000121313566674122027033 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" namespace Quotient { // Data structures /// Used by clients to discover homeserver information. struct HomeserverInformation { /// The base URL for the homeserver for client-server connections. QString baseUrl; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const HomeserverInformation& pod); static void fillFrom(const QJsonObject& jo, HomeserverInformation& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/definitions/wellknown/homeserver.cpp0000644000175000000620000000104513566674122027371 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "homeserver.h" using namespace Quotient; void JsonObjectConverter::dumpTo( QJsonObject& jo, const HomeserverInformation& pod) { addParam<>(jo, QStringLiteral("base_url"), pod.baseUrl); } void JsonObjectConverter::fillFrom( const QJsonObject& jo, HomeserverInformation& result) { fromJson(jo.value("base_url"_ls), result.baseUrl); } spectral/include/libQuotient/lib/csapi/definitions/wellknown/full.cpp0000644000175000000620000000146213566674122026157 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "full.h" using namespace Quotient; void JsonObjectConverter::dumpTo( QJsonObject& jo, const DiscoveryInformation& pod) { fillJson(jo, pod.additionalProperties); addParam<>(jo, QStringLiteral("m.homeserver"), pod.homeserver); addParam(jo, QStringLiteral("m.identity_server"), pod.identityServer); } void JsonObjectConverter::fillFrom( QJsonObject jo, DiscoveryInformation& result) { fromJson(jo.take("m.homeserver"_ls), result.homeserver); fromJson(jo.take("m.identity_server"_ls), result.identityServer); fromJson(jo, result.additionalProperties); } spectral/include/libQuotient/lib/csapi/definitions/wellknown/full.h0000644000175000000620000000242013566674122025617 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/definitions/wellknown/homeserver.h" #include "csapi/definitions/wellknown/identity_server.h" #include #include namespace Quotient { // Data structures /// Used by clients to determine the homeserver, identity server, and other/// /// optional components they should be interacting with. struct DiscoveryInformation { /// Used by clients to determine the homeserver, identity server, and /// otheroptional components they should be interacting with. HomeserverInformation homeserver; /// Used by clients to determine the homeserver, identity server, and /// otheroptional components they should be interacting with. Omittable identityServer; /// Application-dependent keys using Java package naming convention. QHash additionalProperties; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const DiscoveryInformation& pod); static void fillFrom(QJsonObject jo, DiscoveryInformation& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/definitions/wellknown/identity_server.h0000644000175000000620000000124513566674122030100 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" namespace Quotient { // Data structures /// Used by clients to discover identity server information. struct IdentityServerInformation { /// The base URL for the identity server for client-server connections. QString baseUrl; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const IdentityServerInformation& pod); static void fillFrom(const QJsonObject& jo, IdentityServerInformation& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/definitions/client_device.h0000644000175000000620000000172313566674122025437 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" namespace Quotient { // Data structures /// A client device struct Device { /// Identifier of this device. QString deviceId; /// Display name set by the user for this device. Absent if no name has /// beenset. QString displayName; /// The IP address where this device was last seen. (May be a few minutes /// outof date, for efficiency reasons). QString lastSeenIp; /// The timestamp (in milliseconds since the unix epoch) when this deviceswas /// last seen. (May be a few minutes out of date, for efficiencyreasons). Omittable lastSeenTs; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const Device& pod); static void fillFrom(const QJsonObject& jo, Device& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/definitions/device_keys.h0000644000175000000620000000250513566674122025133 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include namespace Quotient { // Data structures /// Device identity keys struct DeviceKeys { /// The ID of the user the device belongs to. Must match the user ID /// usedwhen logging in. QString userId; /// The ID of the device these keys belong to. Must match the device ID /// usedwhen logging in. QString deviceId; /// The encryption algorithms supported by this device. QStringList algorithms; /// Public identity keys. The names of the properties should be in theformat /// ``:``. The keys themselves should beencoded as /// specified by the key algorithm. QHash keys; /// Signatures for the device key object. A map from user ID, to a map /// from``:`` to the signature.The signature is /// calculated using the process described at `SigningJSON`_. QHash> signatures; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const DeviceKeys& pod); static void fillFrom(const QJsonObject& jo, DeviceKeys& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/definitions/event_filter.h0000644000175000000620000000257013566674122025331 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" namespace Quotient { // Data structures struct EventFilter { /// The maximum number of events to return. Omittable limit; /// A list of sender IDs to exclude. If this list is absent then no senders /// are excluded. A matching sender will be excluded even if it is listed in /// the ``'senders'`` filter. QStringList notSenders; /// A list of event types to exclude. If this list is absent then no event /// types are excluded. A matching type will be excluded even if it is /// listed in the ``'types'`` filter. A '*' can be used as a wildcard to /// match any sequence of characters. QStringList notTypes; /// A list of senders IDs to include. If this list is absent then all /// senders are included. QStringList senders; /// A list of event types to include. If this list is absent then all event /// types are included. A ``'*'`` can be used as a wildcard to match any /// sequence of characters. QStringList types; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const EventFilter& pod); static void fillFrom(const QJsonObject& jo, EventFilter& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/definitions/device_keys.cpp0000644000175000000620000000204213566674122025462 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "device_keys.h" using namespace Quotient; void JsonObjectConverter::dumpTo(QJsonObject& jo, const DeviceKeys& pod) { addParam<>(jo, QStringLiteral("user_id"), pod.userId); addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); addParam<>(jo, QStringLiteral("algorithms"), pod.algorithms); addParam<>(jo, QStringLiteral("keys"), pod.keys); addParam<>(jo, QStringLiteral("signatures"), pod.signatures); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, DeviceKeys& result) { fromJson(jo.value("user_id"_ls), result.userId); fromJson(jo.value("device_id"_ls), result.deviceId); fromJson(jo.value("algorithms"_ls), result.algorithms); fromJson(jo.value("keys"_ls), result.keys); fromJson(jo.value("signatures"_ls), result.signatures); } spectral/include/libQuotient/lib/csapi/definitions/push_condition.cpp0000644000175000000620000000164313566674122026223 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "push_condition.h" using namespace Quotient; void JsonObjectConverter::dumpTo(QJsonObject& jo, const PushCondition& pod) { addParam<>(jo, QStringLiteral("kind"), pod.kind); addParam(jo, QStringLiteral("key"), pod.key); addParam(jo, QStringLiteral("pattern"), pod.pattern); addParam(jo, QStringLiteral("is"), pod.is); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, PushCondition& result) { fromJson(jo.value("kind"_ls), result.kind); fromJson(jo.value("key"_ls), result.key); fromJson(jo.value("pattern"_ls), result.pattern); fromJson(jo.value("is"_ls), result.is); } spectral/include/libQuotient/lib/csapi/definitions/push_rule.cpp0000644000175000000620000000215513566674122025203 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "push_rule.h" using namespace Quotient; void JsonObjectConverter::dumpTo(QJsonObject& jo, const PushRule& pod) { addParam<>(jo, QStringLiteral("actions"), pod.actions); addParam<>(jo, QStringLiteral("default"), pod.isDefault); addParam<>(jo, QStringLiteral("enabled"), pod.enabled); addParam<>(jo, QStringLiteral("rule_id"), pod.ruleId); addParam(jo, QStringLiteral("conditions"), pod.conditions); addParam(jo, QStringLiteral("pattern"), pod.pattern); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, PushRule& result) { fromJson(jo.value("actions"_ls), result.actions); fromJson(jo.value("default"_ls), result.isDefault); fromJson(jo.value("enabled"_ls), result.enabled); fromJson(jo.value("rule_id"_ls), result.ruleId); fromJson(jo.value("conditions"_ls), result.conditions); fromJson(jo.value("pattern"_ls), result.pattern); } spectral/include/libQuotient/lib/csapi/definitions/sync_filter.cpp0000644000175000000620000000567213566674122025525 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "sync_filter.h" using namespace Quotient; void JsonObjectConverter::dumpTo(QJsonObject& jo, const StateFilter& pod) { fillJson(jo, pod); addParam(jo, QStringLiteral("lazy_load_members"), pod.lazyLoadMembers); addParam(jo, QStringLiteral("include_redundant_members"), pod.includeRedundantMembers); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, StateFilter& result) { fillFromJson(jo, result); fromJson(jo.value("lazy_load_members"_ls), result.lazyLoadMembers); fromJson(jo.value("include_redundant_members"_ls), result.includeRedundantMembers); } void JsonObjectConverter::dumpTo(QJsonObject& jo, const RoomFilter& pod) { addParam(jo, QStringLiteral("not_rooms"), pod.notRooms); addParam(jo, QStringLiteral("rooms"), pod.rooms); addParam(jo, QStringLiteral("ephemeral"), pod.ephemeral); addParam(jo, QStringLiteral("include_leave"), pod.includeLeave); addParam(jo, QStringLiteral("state"), pod.state); addParam(jo, QStringLiteral("timeline"), pod.timeline); addParam(jo, QStringLiteral("account_data"), pod.accountData); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, RoomFilter& result) { fromJson(jo.value("not_rooms"_ls), result.notRooms); fromJson(jo.value("rooms"_ls), result.rooms); fromJson(jo.value("ephemeral"_ls), result.ephemeral); fromJson(jo.value("include_leave"_ls), result.includeLeave); fromJson(jo.value("state"_ls), result.state); fromJson(jo.value("timeline"_ls), result.timeline); fromJson(jo.value("account_data"_ls), result.accountData); } void JsonObjectConverter::dumpTo(QJsonObject& jo, const Filter& pod) { addParam(jo, QStringLiteral("event_fields"), pod.eventFields); addParam(jo, QStringLiteral("event_format"), pod.eventFormat); addParam(jo, QStringLiteral("presence"), pod.presence); addParam(jo, QStringLiteral("account_data"), pod.accountData); addParam(jo, QStringLiteral("room"), pod.room); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, Filter& result) { fromJson(jo.value("event_fields"_ls), result.eventFields); fromJson(jo.value("event_format"_ls), result.eventFormat); fromJson(jo.value("presence"_ls), result.presence); fromJson(jo.value("account_data"_ls), result.accountData); fromJson(jo.value("room"_ls), result.room); } spectral/include/libQuotient/lib/csapi/definitions/auth_data.h0000644000175000000620000000157113566674122024575 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include #include namespace Quotient { // Data structures /// Used by clients to submit authentication information to the /// interactive-authentication API struct AuthenticationData { /// The login type that the client is attempting to complete. QString type; /// The value of the session key given by the homeserver. QString session; /// Keys dependent on the login type QHash authInfo; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const AuthenticationData& pod); static void fillFrom(QJsonObject jo, AuthenticationData& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/definitions/push_condition.h0000644000175000000620000000224113566674122025663 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" namespace Quotient { // Data structures struct PushCondition { QString kind; /// Required for ``event_match`` conditions. The dot-separated field of /// theevent to match. QString key; /// Required for ``event_match`` conditions. The glob-style pattern tomatch /// against. Patterns with no special glob characters should betreated as /// having asterisks prepended and appended when testing thecondition. QString pattern; /// Required for ``room_member_count`` conditions. A decimal integeroptionally /// prefixed by one of, ==, <, >, >= or <=. A prefix of < matchesrooms where /// the member count is strictly less than the given number andso forth. If /// no prefix is present, this parameter defaults to ==. QString is; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const PushCondition& pod); static void fillFrom(const QJsonObject& jo, PushCondition& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/definitions/push_ruleset.cpp0000644000175000000620000000210713566674122025714 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "push_ruleset.h" using namespace Quotient; void JsonObjectConverter::dumpTo(QJsonObject& jo, const PushRuleset& pod) { addParam(jo, QStringLiteral("content"), pod.content); addParam(jo, QStringLiteral("override"), pod.override); addParam(jo, QStringLiteral("room"), pod.room); addParam(jo, QStringLiteral("sender"), pod.sender); addParam(jo, QStringLiteral("underride"), pod.underride); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, PushRuleset& result) { fromJson(jo.value("content"_ls), result.content); fromJson(jo.value("override"_ls), result.override); fromJson(jo.value("room"_ls), result.room); fromJson(jo.value("sender"_ls), result.sender); fromJson(jo.value("underride"_ls), result.underride); } spectral/include/libQuotient/lib/csapi/definitions/user_identifier.h0000644000175000000620000000140213566674122026014 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include namespace Quotient { // Data structures /// Identification information for a user struct UserIdentifier { /// The type of identification. See `Identifier types`_ for supported /// values and additional property descriptions. QString type; /// Identification information for a user QVariantHash additionalProperties; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const UserIdentifier& pod); static void fillFrom(QJsonObject jo, UserIdentifier& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/definitions/client_device.cpp0000644000175000000620000000161413566674122025771 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "client_device.h" using namespace Quotient; void JsonObjectConverter::dumpTo(QJsonObject& jo, const Device& pod) { addParam<>(jo, QStringLiteral("device_id"), pod.deviceId); addParam(jo, QStringLiteral("display_name"), pod.displayName); addParam(jo, QStringLiteral("last_seen_ip"), pod.lastSeenIp); addParam(jo, QStringLiteral("last_seen_ts"), pod.lastSeenTs); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, Device& result) { fromJson(jo.value("device_id"_ls), result.deviceId); fromJson(jo.value("display_name"_ls), result.displayName); fromJson(jo.value("last_seen_ip"_ls), result.lastSeenIp); fromJson(jo.value("last_seen_ts"_ls), result.lastSeenTs); } spectral/include/libQuotient/lib/csapi/definitions/user_identifier.cpp0000644000175000000620000000125513566674122026355 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "user_identifier.h" using namespace Quotient; void JsonObjectConverter::dumpTo(QJsonObject& jo, const UserIdentifier& pod) { fillJson(jo, pod.additionalProperties); addParam<>(jo, QStringLiteral("type"), pod.type); } void JsonObjectConverter::fillFrom(QJsonObject jo, UserIdentifier& result) { fromJson(jo.take("type"_ls), result.type); fromJson(jo, result.additionalProperties); } spectral/include/libQuotient/lib/csapi/definitions/room_event_filter.h0000644000175000000620000000210713566674122026361 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "csapi/definitions/event_filter.h" namespace Quotient { // Data structures struct RoomEventFilter : EventFilter { /// A list of room IDs to exclude. If this list is absent then no rooms are /// excluded. A matching room will be excluded even if it is listed in the /// ``'rooms'`` filter. QStringList notRooms; /// A list of room IDs to include. If this list is absent then all rooms are /// included. QStringList rooms; /// If ``true``, includes only events with a ``url`` key in their content. /// If ``false``, excludes those events. If omitted, ``url`` key is not /// considered for filtering. Omittable containsUrl; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const RoomEventFilter& pod); static void fillFrom(const QJsonObject& jo, RoomEventFilter& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/openid.h0000644000175000000620000000373613566674122021613 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" #include "jobs/basejob.h" #include namespace Quotient { // Operations /// Get an OpenID token object to verify the requester's identity. /*! * Gets an OpenID token object that the requester may supply to another * service to verify their identity in Matrix. The generated token is only * valid for exchanging for user information from the federation API for * OpenID. * * The access token generated is only valid for the OpenID API. It cannot * be used to request another OpenID access token or call ``/sync``, for * example. */ class RequestOpenIdTokenJob : public BaseJob { public: /*! Get an OpenID token object to verify the requester's identity. * \param userId * The user to request and OpenID token for. Should be the user who * is authenticated for the request. * \param body * An empty object. Reserved for future expansion. */ explicit RequestOpenIdTokenJob(const QString& userId, const QJsonObject& body = {}); ~RequestOpenIdTokenJob() override; // Result properties /// An access token the consumer may use to verify the identity of /// the person who generated the token. This is given to the federation /// API ``GET /openid/userinfo``. const QString& accessToken() const; /// The string ``Bearer``. const QString& tokenType() const; /// The homeserver domain the consumer should use when attempting to /// verify the user's identity. const QString& matrixServerName() const; /// The number of seconds before this token expires and a new one must /// be generated. int expiresIn() const; protected: Status parseJson(const QJsonDocument& data) override; private: class Private; QScopedPointer d; }; } // namespace Quotient spectral/include/libQuotient/lib/csapi/logout.cpp0000644000175000000620000000162313566674122022172 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "logout.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); QUrl LogoutJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/logout"); } static const auto LogoutJobName = QStringLiteral("LogoutJob"); LogoutJob::LogoutJob() : BaseJob(HttpVerb::Post, LogoutJobName, basePath % "/logout") {} QUrl LogoutAllJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/logout/all"); } static const auto LogoutAllJobName = QStringLiteral("LogoutAllJob"); LogoutAllJob::LogoutAllJob() : BaseJob(HttpVerb::Post, LogoutAllJobName, basePath % "/logout/all") {} spectral/include/libQuotient/lib/csapi/appservice_room_directory.cpp0000644000175000000620000000162113566674122026140 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "appservice_room_directory.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); static const auto UpdateAppserviceRoomDirectoryVsibilityJobName = QStringLiteral("UpdateAppserviceRoomDirectoryVsibilityJob"); UpdateAppserviceRoomDirectoryVsibilityJob::UpdateAppserviceRoomDirectoryVsibilityJob( const QString& networkId, const QString& roomId, const QString& visibility) : BaseJob(HttpVerb::Put, UpdateAppserviceRoomDirectoryVsibilityJobName, basePath % "/directory/list/appservice/" % networkId % "/" % roomId) { QJsonObject _data; addParam<>(_data, QStringLiteral("visibility"), visibility); setRequestData(_data); } spectral/include/libQuotient/lib/csapi/content-repo.cpp0000644000175000000620000002301713566674122023277 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "content-repo.h" #include "converters.h" #include #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/media/r0"); class UploadContentJob::Private { public: QString contentUri; }; BaseJob::Query queryToUploadContent(const QString& filename) { BaseJob::Query _q; addParam(_q, QStringLiteral("filename"), filename); return _q; } static const auto UploadContentJobName = QStringLiteral("UploadContentJob"); UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, const QString& contentType) : BaseJob(HttpVerb::Post, UploadContentJobName, basePath % "/upload", queryToUploadContent(filename)) , d(new Private) { setRequestHeader("Content-Type", contentType.toLatin1()); setRequestData(Data(content)); } UploadContentJob::~UploadContentJob() = default; const QString& UploadContentJob::contentUri() const { return d->contentUri; } BaseJob::Status UploadContentJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("content_uri"_ls)) return { IncorrectResponse, "The key 'content_uri' not found in the response" }; fromJson(json.value("content_uri"_ls), d->contentUri); return Success; } class GetContentJob::Private { public: QString contentType; QString contentDisposition; QIODevice* data; }; BaseJob::Query queryToGetContent(bool allowRemote) { BaseJob::Query _q; addParam(_q, QStringLiteral("allow_remote"), allowRemote); return _q; } QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/download/" % serverName % "/" % mediaId, queryToGetContent(allowRemote)); } static const auto GetContentJobName = QStringLiteral("GetContentJob"); GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId, bool allowRemote) : BaseJob(HttpVerb::Get, GetContentJobName, basePath % "/download/" % serverName % "/" % mediaId, queryToGetContent(allowRemote), {}, false) , d(new Private) { setExpectedContentTypes({ "*/*" }); } GetContentJob::~GetContentJob() = default; const QString& GetContentJob::contentType() const { return d->contentType; } const QString& GetContentJob::contentDisposition() const { return d->contentDisposition; } QIODevice* GetContentJob::data() const { return d->data; } BaseJob::Status GetContentJob::parseReply(QNetworkReply* reply) { d->contentType = reply->rawHeader("Content-Type"); d->contentDisposition = reply->rawHeader("Content-Disposition"); d->data = reply; return Success; } class GetContentOverrideNameJob::Private { public: QString contentType; QString contentDisposition; QIODevice* data; }; BaseJob::Query queryToGetContentOverrideName(bool allowRemote) { BaseJob::Query _q; addParam(_q, QStringLiteral("allow_remote"), allowRemote); return _q; } QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, const QString& fileName, bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/download/" % serverName % "/" % mediaId % "/" % fileName, queryToGetContentOverrideName(allowRemote)); } static const auto GetContentOverrideNameJobName = QStringLiteral("GetContentOverrideNameJob"); GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, const QString& mediaId, const QString& fileName, bool allowRemote) : BaseJob(HttpVerb::Get, GetContentOverrideNameJobName, basePath % "/download/" % serverName % "/" % mediaId % "/" % fileName, queryToGetContentOverrideName(allowRemote), {}, false) , d(new Private) { setExpectedContentTypes({ "*/*" }); } GetContentOverrideNameJob::~GetContentOverrideNameJob() = default; const QString& GetContentOverrideNameJob::contentType() const { return d->contentType; } const QString& GetContentOverrideNameJob::contentDisposition() const { return d->contentDisposition; } QIODevice* GetContentOverrideNameJob::data() const { return d->data; } BaseJob::Status GetContentOverrideNameJob::parseReply(QNetworkReply* reply) { d->contentType = reply->rawHeader("Content-Type"); d->contentDisposition = reply->rawHeader("Content-Disposition"); d->data = reply; return Success; } class GetContentThumbnailJob::Private { public: QString contentType; QIODevice* data; }; BaseJob::Query queryToGetContentThumbnail(int width, int height, const QString& method, bool allowRemote) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("width"), width); addParam<>(_q, QStringLiteral("height"), height); addParam(_q, QStringLiteral("method"), method); addParam(_q, QStringLiteral("allow_remote"), allowRemote); return _q; } QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, int width, int height, const QString& method, bool allowRemote) { return BaseJob::makeRequestUrl( std::move(baseUrl), basePath % "/thumbnail/" % serverName % "/" % mediaId, queryToGetContentThumbnail(width, height, method, allowRemote)); } static const auto GetContentThumbnailJobName = QStringLiteral("GetContentThumbnailJob"); GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, const QString& mediaId, int width, int height, const QString& method, bool allowRemote) : BaseJob(HttpVerb::Get, GetContentThumbnailJobName, basePath % "/thumbnail/" % serverName % "/" % mediaId, queryToGetContentThumbnail(width, height, method, allowRemote), {}, false) , d(new Private) { setExpectedContentTypes({ "image/jpeg", "image/png" }); } GetContentThumbnailJob::~GetContentThumbnailJob() = default; const QString& GetContentThumbnailJob::contentType() const { return d->contentType; } QIODevice* GetContentThumbnailJob::data() const { return d->data; } BaseJob::Status GetContentThumbnailJob::parseReply(QNetworkReply* reply) { d->contentType = reply->rawHeader("Content-Type"); d->data = reply; return Success; } class GetUrlPreviewJob::Private { public: Omittable matrixImageSize; QString ogImage; }; BaseJob::Query queryToGetUrlPreview(const QString& url, Omittable ts) { BaseJob::Query _q; addParam<>(_q, QStringLiteral("url"), url); addParam(_q, QStringLiteral("ts"), ts); return _q; } QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QString& url, Omittable ts) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/preview_url", queryToGetUrlPreview(url, ts)); } static const auto GetUrlPreviewJobName = QStringLiteral("GetUrlPreviewJob"); GetUrlPreviewJob::GetUrlPreviewJob(const QString& url, Omittable ts) : BaseJob(HttpVerb::Get, GetUrlPreviewJobName, basePath % "/preview_url", queryToGetUrlPreview(url, ts)) , d(new Private) {} GetUrlPreviewJob::~GetUrlPreviewJob() = default; Omittable GetUrlPreviewJob::matrixImageSize() const { return d->matrixImageSize; } const QString& GetUrlPreviewJob::ogImage() const { return d->ogImage; } BaseJob::Status GetUrlPreviewJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("matrix:image:size"_ls), d->matrixImageSize); fromJson(json.value("og:image"_ls), d->ogImage); return Success; } class GetConfigJob::Private { public: Omittable uploadSize; }; QUrl GetConfigJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), basePath % "/config"); } static const auto GetConfigJobName = QStringLiteral("GetConfigJob"); GetConfigJob::GetConfigJob() : BaseJob(HttpVerb::Get, GetConfigJobName, basePath % "/config") , d(new Private) {} GetConfigJob::~GetConfigJob() = default; Omittable GetConfigJob::uploadSize() const { return d->uploadSize; } BaseJob::Status GetConfigJob::parseJson(const QJsonDocument& data) { auto json = data.object(); fromJson(json.value("m.upload.size"_ls), d->uploadSize); return Success; } spectral/include/libQuotient/lib/csapi/to_device.cpp0000644000175000000620000000143013566674122022616 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "to_device.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); static const auto SendToDeviceJobName = QStringLiteral("SendToDeviceJob"); SendToDeviceJob::SendToDeviceJob( const QString& eventType, const QString& txnId, const QHash>& messages) : BaseJob(HttpVerb::Put, SendToDeviceJobName, basePath % "/sendToDevice/" % eventType % "/" % txnId) { QJsonObject _data; addParam(_data, QStringLiteral("messages"), messages); setRequestData(_data); } spectral/include/libQuotient/lib/csapi/create_room.cpp0000644000175000000620000000655313566674122023167 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "create_room.h" #include "converters.h" #include using namespace Quotient; static const auto basePath = QStringLiteral("/_matrix/client/r0"); // Converters namespace Quotient { template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const CreateRoomJob::Invite3pid& pod) { addParam<>(jo, QStringLiteral("id_server"), pod.idServer); addParam<>(jo, QStringLiteral("medium"), pod.medium); addParam<>(jo, QStringLiteral("address"), pod.address); } }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const CreateRoomJob::StateEvent& pod) { addParam<>(jo, QStringLiteral("type"), pod.type); addParam(jo, QStringLiteral("state_key"), pod.stateKey); addParam<>(jo, QStringLiteral("content"), pod.content); } }; } // namespace Quotient class CreateRoomJob::Private { public: QString roomId; }; static const auto CreateRoomJobName = QStringLiteral("CreateRoomJob"); CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& roomAliasName, const QString& name, const QString& topic, const QStringList& invite, const QVector& invite3pid, const QString& roomVersion, const QJsonObject& creationContent, const QVector& initialState, const QString& preset, Omittable isDirect, const QJsonObject& powerLevelContentOverride) : BaseJob(HttpVerb::Post, CreateRoomJobName, basePath % "/createRoom") , d(new Private) { QJsonObject _data; addParam(_data, QStringLiteral("visibility"), visibility); addParam(_data, QStringLiteral("room_alias_name"), roomAliasName); addParam(_data, QStringLiteral("name"), name); addParam(_data, QStringLiteral("topic"), topic); addParam(_data, QStringLiteral("invite"), invite); addParam(_data, QStringLiteral("invite_3pid"), invite3pid); addParam(_data, QStringLiteral("room_version"), roomVersion); addParam(_data, QStringLiteral("creation_content"), creationContent); addParam(_data, QStringLiteral("initial_state"), initialState); addParam(_data, QStringLiteral("preset"), preset); addParam(_data, QStringLiteral("is_direct"), isDirect); addParam(_data, QStringLiteral("power_level_content_override"), powerLevelContentOverride); setRequestData(_data); } CreateRoomJob::~CreateRoomJob() = default; const QString& CreateRoomJob::roomId() const { return d->roomId; } BaseJob::Status CreateRoomJob::parseJson(const QJsonDocument& data) { auto json = data.object(); if (!json.contains("room_id"_ls)) return { IncorrectResponse, "The key 'room_id' not found in the response" }; fromJson(json.value("room_id"_ls), d->roomId); return Success; } spectral/include/libQuotient/lib/syncdata.cpp0000644000175000000620000002004513566674122021367 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "syncdata.h" #include "events/eventloader.h" #include #include using namespace Quotient; const QString SyncRoomData::UnreadCountKey = QStringLiteral("x-quotient.unread_count"); bool RoomSummary::isEmpty() const { return joinedMemberCount.omitted() && invitedMemberCount.omitted() && heroes.omitted(); } bool RoomSummary::merge(const RoomSummary& other) { // Using bitwise OR to prevent computation shortcut. return joinedMemberCount.merge(other.joinedMemberCount) | invitedMemberCount.merge(other.invitedMemberCount) | heroes.merge(other.heroes); } QDebug Quotient::operator<<(QDebug dbg, const RoomSummary& rs) { QDebugStateSaver _(dbg); QStringList sl; if (!rs.joinedMemberCount.omitted()) sl << QStringLiteral("joined: %1").arg(rs.joinedMemberCount.value()); if (!rs.invitedMemberCount.omitted()) sl << QStringLiteral("invited: %1").arg(rs.invitedMemberCount.value()); if (!rs.heroes.omitted()) sl << QStringLiteral("heroes: [%1]").arg(rs.heroes.value().join(',')); dbg.nospace().noquote() << sl.join(QStringLiteral("; ")); return dbg; } void JsonObjectConverter::dumpTo(QJsonObject& jo, const RoomSummary& rs) { addParam(jo, QStringLiteral("m.joined_member_count"), rs.joinedMemberCount); addParam(jo, QStringLiteral("m.invited_member_count"), rs.invitedMemberCount); addParam(jo, QStringLiteral("m.heroes"), rs.heroes); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, RoomSummary& rs) { fromJson(jo["m.joined_member_count"_ls], rs.joinedMemberCount); fromJson(jo["m.invited_member_count"_ls], rs.invitedMemberCount); fromJson(jo["m.heroes"_ls], rs.heroes); } template inline EventsArrayT load(const QJsonObject& batches, StrT keyName) { return fromJson(batches[keyName].toObject().value("events"_ls)); } SyncRoomData::SyncRoomData(const QString& roomId_, JoinState joinState_, const QJsonObject& room_) : roomId(roomId_) , joinState(joinState_) , summary(fromJson(room_["summary"_ls])) , state(load(room_, joinState == JoinState::Invite ? "invite_state"_ls : "state"_ls)) { switch (joinState) { case JoinState::Join: ephemeral = load(room_, "ephemeral"_ls); [[fallthrough]]; case JoinState::Leave: { accountData = load(room_, "account_data"_ls); timeline = load(room_, "timeline"_ls); const auto timelineJson = room_.value("timeline"_ls).toObject(); timelineLimited = timelineJson.value("limited"_ls).toBool(); timelinePrevBatch = timelineJson.value("prev_batch"_ls).toString(); break; } default: /* nothing on top of state */; } const auto unreadJson = room_.value("unread_notifications"_ls).toObject(); unreadCount = unreadJson.value(UnreadCountKey).toInt(-2); highlightCount = unreadJson.value("highlight_count"_ls).toInt(); notificationCount = unreadJson.value("notification_count"_ls).toInt(); if (highlightCount > 0 || notificationCount > 0) qCDebug(SYNCJOB) << "Room" << roomId_ << "has highlights:" << highlightCount << "and notifications:" << notificationCount; } SyncData::SyncData(const QString& cacheFileName) { QFileInfo cacheFileInfo { cacheFileName }; auto json = loadJson(cacheFileName); auto requiredVersion = std::get<0>(cacheVersion()); auto actualVersion = json.value("cache_version"_ls).toObject().value("major"_ls).toInt(); if (actualVersion == requiredVersion) parseJson(json, cacheFileInfo.absolutePath() + '/'); else qCWarning(MAIN) << "Major version of the cache file is" << actualVersion << "but" << requiredVersion << "is required; discarding the cache"; } SyncDataList&& SyncData::takeRoomData() { return move(roomData); } QString SyncData::fileNameForRoom(QString roomId) { roomId.replace(':', '_'); return roomId + ".json"; } Events&& SyncData::takePresenceData() { return std::move(presenceData); } Events&& SyncData::takeAccountData() { return std::move(accountData); } Events&& SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); } QJsonObject SyncData::loadJson(const QString& fileName) { QFile roomFile { fileName }; if (!roomFile.exists()) { qCWarning(MAIN) << "No state cache file" << fileName; return {}; } if (!roomFile.open(QIODevice::ReadOnly)) { qCWarning(MAIN) << "Failed to open state cache file" << roomFile.fileName(); return {}; } auto data = roomFile.readAll(); const auto json = (data.startsWith('{') ? QJsonDocument::fromJson(data) : QJsonDocument::fromBinaryData(data)) .object(); if (json.isEmpty()) { qCWarning(MAIN) << "State cache in" << fileName << "is broken or empty, discarding"; } return json; } void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) { QElapsedTimer et; et.start(); nextBatch_ = json.value("next_batch"_ls).toString(); presenceData = load(json, "presence"_ls); accountData = load(json, "account_data"_ls); toDeviceEvents = load(json, "to_device"_ls); auto rooms = json.value("rooms"_ls).toObject(); JoinStates::Int ii = 1; // ii is used to make a JoinState value auto totalRooms = 0; auto totalEvents = 0; for (size_t i = 0; i < JoinStateStrings.size(); ++i, ii <<= 1) { const auto rs = rooms.value(JoinStateStrings[i]).toObject(); // We have a Qt container on the right and an STL one on the left roomData.reserve(static_cast(rs.size())); for (auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) { auto roomJson = roomIt->isObject() ? roomIt->toObject() : loadJson(baseDir + fileNameForRoom(roomIt.key())); if (roomJson.isEmpty()) { unresolvedRoomIds.push_back(roomIt.key()); continue; } roomData.emplace_back(roomIt.key(), JoinState(ii), roomJson); const auto& r = roomData.back(); totalEvents += r.state.size() + r.ephemeral.size() + r.accountData.size() + r.timeline.size(); } totalRooms += rs.size(); } if (!unresolvedRoomIds.empty()) qCWarning(MAIN) << "Unresolved rooms:" << unresolvedRoomIds.join(','); if (totalRooms > 9 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** SyncData::parseJson(): batch with" << totalRooms << "room(s)," << totalEvents << "event(s) in" << et; } spectral/include/libQuotient/lib/joinstate.h0000644000175000000620000000310613566674122021225 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include #include namespace Quotient { enum class JoinState : unsigned int { Join = 0x1, Invite = 0x2, Leave = 0x4, }; Q_DECLARE_FLAGS(JoinStates, JoinState) // We cannot use Q_ENUM outside of a Q_OBJECT and besides, we want // to use strings that match respective JSON keys. static const std::array JoinStateStrings { { "join", "invite", "leave" } }; inline const char* toCString(JoinState js) { size_t state = size_t(js), index = 0; while (state >>= 1u) ++index; return JoinStateStrings[index]; } } // namespace Quotient Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::JoinStates) spectral/include/libQuotient/lib/logging.h0000644000175000000620000000533713566674122020663 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include #include Q_DECLARE_LOGGING_CATEGORY(MAIN) Q_DECLARE_LOGGING_CATEGORY(STATE) Q_DECLARE_LOGGING_CATEGORY(MESSAGES) Q_DECLARE_LOGGING_CATEGORY(EVENTS) Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL) Q_DECLARE_LOGGING_CATEGORY(E2EE) Q_DECLARE_LOGGING_CATEGORY(JOBS) Q_DECLARE_LOGGING_CATEGORY(SYNCJOB) Q_DECLARE_LOGGING_CATEGORY(PROFILER) namespace Quotient { // QDebug manipulators using QDebugManip = QDebug (*)(QDebug); /** * @brief QDebug manipulator to setup the stream for JSON output * * Originally made to encapsulate the change in QDebug behavior in Qt 5.4 * and the respective addition of QDebug::noquote(). * Together with the operator<<() helper, the proposed usage is * (similar to std:: I/O manipulators): * * @example qCDebug() << formatJson << json_object; // (QJsonObject, etc.) */ inline QDebug formatJson(QDebug debug_object) { #if QT_VERSION < QT_VERSION_CHECK(5, 4, 0) return debug_object; #else return debug_object.noquote(); #endif } /** * @brief A helper operator to facilitate usage of formatJson (and possibly * other manipulators) * * @param debug_object to output the json to * @param qdm a QDebug manipulator * @return a copy of debug_object that has its mode altered by qdm */ inline QDebug operator<<(QDebug debug_object, QDebugManip qdm) { return qdm(debug_object); } inline qint64 profilerMinNsecs() { return #ifdef PROFILER_LOG_USECS PROFILER_LOG_USECS #else 200 #endif * 1000; } } // namespace Quotient /// \deprecated Use namespace Quotient instead namespace QMatrixClient = Quotient; inline QDebug operator<<(QDebug debug_object, const QElapsedTimer& et) { auto val = et.nsecsElapsed() / 1000; if (val < 1000) debug_object << val << "µs"; else debug_object << val / 1000 << "ms"; return debug_object; } spectral/include/libQuotient/lib/connection.cpp0000644000175000000620000015057413566674122021733 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "connection.h" #include "connectiondata.h" #include "encryptionmanager.h" #include "room.h" #include "settings.h" #include "user.h" #include "csapi/account-data.h" #include "csapi/capabilities.h" #include "csapi/joining.h" #include "csapi/leaving.h" #include "csapi/login.h" #include "csapi/logout.h" #include "csapi/receipts.h" #include "csapi/room_send.h" #include "csapi/to_device.h" #include "csapi/versions.h" #include "csapi/voip.h" #include "csapi/wellknown.h" #include "events/directchatevent.h" #include "events/eventloader.h" #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/syncjob.h" #include #include #include #include #include #include #include #include #include using namespace Quotient; // This is very much Qt-specific; STL iterators don't have key() and value() template HashT erase_if(HashT& hashMap, Pred pred) { HashT removals; for (auto it = hashMap.begin(); it != hashMap.end();) { if (pred(it)) { removals.insert(it.key(), it.value()); it = hashMap.erase(it); } else ++it; } return removals; } class Connection::Private { public: explicit Private(std::unique_ptr&& connection) : data(move(connection)) {} Q_DISABLE_COPY(Private) DISABLE_MOVE(Private) Connection* q = nullptr; std::unique_ptr data; // A complex key below is a pair of room name and whether its // state is Invited. The spec mandates to keep Invited room state // separately; specifically, we should keep objects for Invite and // Leave state of the same room if the two happen to co-exist. QHash, Room*> roomMap; /// Mapping from serverparts to alias/room id mappings, /// as of the last sync QHash> roomAliasMap; QVector roomIdsToForget; QVector firstTimeRooms; QVector pendingStateRoomIds; QMap userMap; DirectChatsMap directChats; DirectChatUsersMap directChatUsers; // The below two variables track local changes between sync completions. // See https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events DirectChatsMap dcLocalAdditions; DirectChatsMap dcLocalRemovals; UnorderedMap accountData; int syncLoopTimeout = -1; GetCapabilitiesJob* capabilitiesJob = nullptr; GetCapabilitiesJob::Capabilities capabilities; QScopedPointer encryptionManager; SyncJob* syncJob = nullptr; bool cacheState = true; bool cacheToBinary = SettingsGroup("libQuotient").get("cache_type", SettingsGroup("libQMatrixClient").get("cache_type")) != "json"; bool lazyLoading = false; void connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId); void removeRoom(const QString& roomId); template EventT* unpackAccountData() const { const auto& eventIt = accountData.find(EventT::matrixTypeId()); return eventIt == accountData.end() ? nullptr : weakPtrCast(eventIt->second); } void packAndSendAccountData(EventPtr&& event) { const auto eventType = event->matrixType(); q->callApi(data->userId(), eventType, event->contentJson()); accountData[eventType] = std::move(event); emit q->accountDataChanged(eventType); } template void packAndSendAccountData(ContentT&& content) { packAndSendAccountData( makeEvent(std::forward(content))); } QString topLevelStatePath() const { return q->stateCacheDir().filePath("state.json"); } }; Connection::Connection(const QUrl& server, QObject* parent) : QObject(parent), d(new Private(std::make_unique(server))) { d->q = this; // All d initialization should occur before this line } Connection::Connection(QObject* parent) : Connection({}, parent) {} Connection::~Connection() { qCDebug(MAIN) << "deconstructing connection object for" << userId(); stopSync(); } void Connection::resolveServer(const QString& mxid) { auto maybeBaseUrl = QUrl::fromUserInput(serverPart(mxid)); maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http" if (maybeBaseUrl.isEmpty() || !maybeBaseUrl.isValid()) { emit resolveError(tr("%1 is not a valid homeserver address") .arg(maybeBaseUrl.toString())); return; } setHomeserver(maybeBaseUrl); auto domain = maybeBaseUrl.host(); qCDebug(MAIN) << "Finding the server" << domain; auto getWellKnownJob = callApi(); connect( getWellKnownJob, &BaseJob::finished, [this, getWellKnownJob, maybeBaseUrl] { if (getWellKnownJob->status() == BaseJob::NotFoundError) qCDebug(MAIN) << "No .well-known file, IGNORE"; else { if (getWellKnownJob->status() != BaseJob::Success) { qCDebug(MAIN) << "Fetching .well-known file failed, FAIL_PROMPT"; emit resolveError(tr("Fetching .well-known file failed")); return; } QUrl baseUrl(getWellKnownJob->data().homeserver.baseUrl); if (baseUrl.isEmpty()) { qCDebug(MAIN) << "base_url not provided, FAIL_PROMPT"; emit resolveError(tr("base_url not provided")); return; } if (!baseUrl.isValid()) { qCDebug(MAIN) << "base_url invalid, FAIL_ERROR"; emit resolveError(tr("base_url invalid")); return; } qCDebug(MAIN) << ".well-known for" << maybeBaseUrl.host() << "is" << baseUrl.toString(); setHomeserver(baseUrl); } auto getVersionsJob = callApi(); connect(getVersionsJob, &BaseJob::finished, [this, getVersionsJob] { if (getVersionsJob->status() == BaseJob::Success) { qCDebug(MAIN) << "homeserver url is valid"; emit resolved(); } else { qCDebug(MAIN) << "homeserver url invalid"; emit resolveError(tr("homeserver url invalid")); } }); }); } void Connection::connectToServer(const QString& user, const QString& password, const QString& initialDeviceName, const QString& deviceId) { checkAndConnect(user, [=] { doConnectToServer(user, password, initialDeviceName, deviceId); }); } void Connection::doConnectToServer(const QString& user, const QString& password, const QString& initialDeviceName, const QString& deviceId) { auto loginJob = callApi(QStringLiteral("m.login.password"), UserIdentifier { QStringLiteral("m.id.user"), { { QStringLiteral("user"), user } } }, password, /*token*/ "", deviceId, initialDeviceName); connect(loginJob, &BaseJob::success, this, [this, loginJob] { d->connectWithToken(loginJob->userId(), loginJob->accessToken(), loginJob->deviceId()); AccountSettings accountSettings(loginJob->userId()); d->encryptionManager.reset( new EncryptionManager(accountSettings.encryptionAccountPickle())); if (accountSettings.encryptionAccountPickle().isEmpty()) { accountSettings.setEncryptionAccountPickle( d->encryptionManager->olmAccountPickle()); } d->encryptionManager->uploadIdentityKeys(this); d->encryptionManager->uploadOneTimeKeys(this); }); connect(loginJob, &BaseJob::failure, this, [this, loginJob] { emit loginError(loginJob->errorString(), loginJob->rawDataSample()); }); } void Connection::syncLoopIteration() { sync(d->syncLoopTimeout); } void Connection::connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId) { checkAndConnect(userId, [=] { d->connectWithToken(userId, accessToken, deviceId); }); } void Connection::reloadCapabilities() { d->capabilitiesJob = callApi(BackgroundRequest); connect(d->capabilitiesJob, &BaseJob::finished, this, [this] { if (d->capabilitiesJob->error() == BaseJob::Success) d->capabilities = d->capabilitiesJob->capabilities(); else if (d->capabilitiesJob->error() == BaseJob::IncorrectRequestError) qCDebug(MAIN) << "Server doesn't support /capabilities"; if (d->capabilities.roomVersions.omitted()) { qCWarning(MAIN) << "Pinning supported room version to 1"; d->capabilities.roomVersions = { "1", { { "1", "stable" } } }; } else { qCDebug(MAIN) << "Room versions:" << defaultRoomVersion() << "is default, full list:" << availableRoomVersions(); } Q_ASSERT(!d->capabilities.roomVersions.omitted()); emit capabilitiesLoaded(); for (auto* r : d->roomMap) r->checkVersion(); }); } bool Connection::loadingCapabilities() const { // (Ab)use the fact that room versions cannot be omitted after // the capabilities have been loaded (see reloadCapabilities() above). return d->capabilities.roomVersions.omitted(); } void Connection::Private::connectWithToken(const QString& userId, const QString& accessToken, const QString& deviceId) { data->setUserId(userId); q->user(); // Creates a User object for the local user data->setToken(accessToken.toLatin1()); data->setDeviceId(deviceId); q->setObjectName(userId % '/' % deviceId); qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() << "by user" << userId << "from device" << deviceId; emit q->stateChanged(); emit q->connected(); q->reloadCapabilities(); } void Connection::checkAndConnect(const QString& userId, std::function connectFn) { if (d->data->baseUrl().isValid()) { connectFn(); return; } // Not good to go, try to fix the homeserver URL. if (userId.startsWith('@') && userId.indexOf(':') != -1) { connectSingleShot(this, &Connection::homeserverChanged, this, connectFn); // NB: doResolveServer can emit resolveError, so this is a part of // checkAndConnect function contract. resolveServer(userId); } else emit resolveError(tr("%1 is an invalid homeserver URL") .arg(d->data->baseUrl().toString())); } void Connection::logout() { auto job = callApi(); connect(job, &LogoutJob::finished, this, [job, this] { if (job->status().good() || job->error() == BaseJob::ContentAccessError) { stopSync(); d->data->setToken({}); emit stateChanged(); emit loggedOut(); } }); } void Connection::sync(int timeout) { if (d->syncJob) return; Filter filter; filter.room->timeline->limit = 100; filter.room->state->lazyLoadMembers = d->lazyLoading; auto job = d->syncJob = callApi(BackgroundRequest, d->data->lastEvent(), filter, timeout); connect(job, &SyncJob::success, this, [this, job] { onSyncSuccess(job->takeData()); d->syncJob = nullptr; emit syncDone(); }); connect(job, &SyncJob::retryScheduled, this, [this, job](int retriesTaken, int nextInMilliseconds) { emit networkError(job->errorString(), job->rawDataSample(), retriesTaken, nextInMilliseconds); }); connect(job, &SyncJob::failure, this, [this, job] { d->syncJob = nullptr; if (job->error() == BaseJob::ContentAccessError) { qCWarning(SYNCJOB) << "Sync job failed with ContentAccessError - " "login expired?"; emit loginError(job->errorString(), job->rawDataSample()); } else emit syncError(job->errorString(), job->rawDataSample()); }); } void Connection::syncLoop(int timeout) { d->syncLoopTimeout = timeout; connect(this, &Connection::syncDone, this, &Connection::syncLoopIteration); syncLoopIteration(); // initial sync to start the loop } QJsonObject toJson(const Connection::DirectChatsMap& directChats) { QJsonObject json; for (auto it = directChats.begin(); it != directChats.end();) { QJsonArray roomIds; const auto* user = it.key(); for (; it != directChats.end() && it.key() == user; ++it) roomIds.append(*it); json.insert(user->id(), roomIds); } return json; } void Connection::onSyncSuccess(SyncData&& data, bool fromCache) { d->data->setLastEvent(data.nextBatch()); for (auto&& roomData : data.takeRoomData()) { const auto forgetIdx = d->roomIdsToForget.indexOf(roomData.roomId); if (forgetIdx != -1) { d->roomIdsToForget.removeAt(forgetIdx); if (roomData.joinState == JoinState::Leave) { qDebug(MAIN) << "Room" << roomData.roomId << "has been forgotten, ignoring /sync response for it"; continue; } qWarning(MAIN) << "Room" << roomData.roomId << "has just been forgotten but /sync returned it in" << toCString(roomData.joinState) << "state - suspiciously fast turnaround"; } if (auto* r = provideRoom(roomData.roomId, roomData.joinState)) { d->pendingStateRoomIds.removeOne(roomData.roomId); r->updateData(std::move(roomData), fromCache); if (d->firstTimeRooms.removeOne(r)) { emit loadedRoomState(r); if (!d->capabilities.roomVersions.omitted()) r->checkVersion(); // Otherwise, the version will be checked in reloadCapabilities() } } // Let UI update itself after updating each room QCoreApplication::processEvents(); } // After running this loop, the account data events not saved in // d->accountData (see the end of the loop body) are auto-cleaned away for (auto& eventPtr : data.takeAccountData()) { visit( *eventPtr, [this](const DirectChatEvent& dce) { // https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events const auto& usersToDCs = dce.usersToDirectChats(); DirectChatsMap remoteRemovals = erase_if(d->directChats, [&usersToDCs, this](auto it) { return !(usersToDCs.contains(it.key()->id(), it.value()) || d->dcLocalAdditions.contains(it.key(), it.value())); }); erase_if(d->directChatUsers, [&remoteRemovals](auto it) { return remoteRemovals.contains(it.value(), it.key()); }); // Remove from dcLocalRemovals what the server already has. erase_if(d->dcLocalRemovals, [&remoteRemovals](auto it) { return remoteRemovals.contains(it.key(), it.value()); }); if (MAIN().isDebugEnabled()) for (auto it = remoteRemovals.begin(); it != remoteRemovals.end(); ++it) { qCDebug(MAIN) << it.value() << "is no more a direct chat with" << it.key()->id(); } DirectChatsMap remoteAdditions; for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) { if (auto* u = user(it.key())) { if (!d->directChats.contains(u, it.value()) && !d->dcLocalRemovals.contains(u, it.value())) { Q_ASSERT(!d->directChatUsers.contains(it.value(), u)); remoteAdditions.insert(u, it.value()); d->directChats.insert(u, it.value()); d->directChatUsers.insert(it.value(), u); qCDebug(MAIN) << "Marked room" << it.value() << "as a direct chat with" << u->id(); } } else qCWarning(MAIN) << "Couldn't get a user object for" << it.key(); } // Remove from dcLocalAdditions what the server already has. erase_if(d->dcLocalAdditions, [&remoteAdditions](auto it) { return remoteAdditions.contains(it.key(), it.value()); }); if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty()) emit directChatsListChanged(remoteAdditions, remoteRemovals); }, // catch-all, passing eventPtr for a possible take-over [this, &eventPtr](const Event& accountEvent) { if (is(accountEvent)) qCDebug(MAIN) << "Users ignored by" << userId() << "updated:" << QStringList::fromSet(ignoredUsers()).join(','); auto& currentData = d->accountData[accountEvent.matrixType()]; // A polymorphic event-specific comparison might be a bit // more efficient; maaybe do it another day if (!currentData || currentData->contentJson() != accountEvent.contentJson()) { currentData = std::move(eventPtr); qCDebug(MAIN) << "Updated account data of type" << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); } }); } if (!d->dcLocalAdditions.isEmpty() || !d->dcLocalRemovals.isEmpty()) { qDebug(MAIN) << "Sending updated direct chats to the server:" << d->dcLocalRemovals.size() << "removal(s)," << d->dcLocalAdditions.size() << "addition(s)"; callApi(userId(), QStringLiteral("m.direct"), toJson(d->directChats)); d->dcLocalAdditions.clear(); d->dcLocalRemovals.clear(); } } void Connection::stopSync() { // If there's a sync loop, break it disconnect(this, &Connection::syncDone, this, &Connection::syncLoopIteration); if (d->syncJob) // If there's an ongoing sync job, stop it too { d->syncJob->abandon(); d->syncJob = nullptr; } } QString Connection::nextBatchToken() const { return d->data->lastEvent(); } PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const { return callApi(room->id(), "m.read", event->id()); } JoinRoomJob* Connection::joinRoom(const QString& roomAlias, const QStringList& serverNames) { auto job = callApi(roomAlias, serverNames); // Upon completion, ensure a room object in Join state is created but only // if it's not already there due to a sync completing earlier. connect(job, &JoinRoomJob::success, this, [this, job] { provideRoom(job->roomId()); }); return job; } LeaveRoomJob* Connection::leaveRoom(Room* room) { const auto& roomId = room->id(); const auto job = callApi(roomId); if (room->joinState() == JoinState::Invite) { // Workaround matrix-org/synapse#2181 - if the room is in invite state // the invite may have been cancelled but Synapse didn't send it in // `/sync`. See also #273 for the discussion in the library context. d->pendingStateRoomIds.push_back(roomId); connect(job, &LeaveRoomJob::success, this, [this, roomId] { if (d->pendingStateRoomIds.removeOne(roomId)) { qCDebug(MAIN) << "Forcing the room to Leave status"; provideRoom(roomId, JoinState::Leave); } }); } return job; } inline auto splitMediaId(const QString& mediaId) { auto idParts = mediaId.split('/'); Q_ASSERT_X(idParts.size() == 2, __FUNCTION__, ("'" + mediaId + "' doesn't look like 'serverName/localMediaId'") .toLatin1()); return idParts; } MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId, QSize requestedSize, RunningPolicy policy) const { auto idParts = splitMediaId(mediaId); return callApi(policy, idParts.front(), idParts.back(), requestedSize); } MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, QSize requestedSize, RunningPolicy policy) const { return getThumbnail(url.authority() + url.path(), requestedSize, policy); } MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, int requestedWidth, int requestedHeight, RunningPolicy policy) const { return getThumbnail(url, QSize(requestedWidth, requestedHeight), policy); } UploadContentJob* Connection::uploadContent(QIODevice* contentSource, const QString& filename, const QString& overrideContentType) const { auto contentType = overrideContentType; if (contentType.isEmpty()) { contentType = QMimeDatabase() .mimeTypeForFileNameAndData(filename, contentSource) .name(); contentSource->open(QIODevice::ReadOnly); } return callApi(contentSource, filename, contentType); } UploadContentJob* Connection::uploadFile(const QString& fileName, const QString& overrideContentType) { auto sourceFile = new QFile(fileName); if (!sourceFile->open(QIODevice::ReadOnly)) { qCWarning(MAIN) << "Couldn't open" << sourceFile->fileName() << "for reading"; return nullptr; } return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(), overrideContentType); } GetContentJob* Connection::getContent(const QString& mediaId) const { auto idParts = splitMediaId(mediaId); return callApi(idParts.front(), idParts.back()); } GetContentJob* Connection::getContent(const QUrl& url) const { return getContent(url.authority() + url.path()); } DownloadFileJob* Connection::downloadFile(const QUrl& url, const QString& localFilename) const { auto mediaId = url.authority() + url.path(); auto idParts = splitMediaId(mediaId); auto* job = callApi(idParts.front(), idParts.back(), localFilename); return job; } CreateRoomJob* Connection::createRoom(RoomVisibility visibility, const QString& alias, const QString& name, const QString& topic, QStringList invites, const QString& presetName, const QString& roomVersion, bool isDirect, const QVector& initialState, const QVector& invite3pids, const QJsonObject& creationContent) { invites.removeOne(userId()); // The creator is by definition in the room auto job = callApi(visibility == PublishRoom ? QStringLiteral("public") : QStringLiteral("private"), alias, name, topic, invites, invite3pids, roomVersion, creationContent, initialState, presetName, isDirect); connect(job, &BaseJob::success, this, [this, job, invites, isDirect] { auto* room = provideRoom(job->roomId(), JoinState::Join); if (!room) { Q_ASSERT_X(room, "Connection::createRoom", "Failed to create a room"); return; } emit createdRoom(room); if (isDirect) for (const auto& i : invites) addToDirectChats(room, user(i)); }); return job; } void Connection::requestDirectChat(const QString& userId) { doInDirectChat(userId, [this](Room* r) { emit directChatAvailable(r); }); } void Connection::requestDirectChat(User* u) { doInDirectChat(u, [this](Room* r) { emit directChatAvailable(r); }); } void Connection::doInDirectChat(const QString& userId, const std::function& operation) { if (auto* u = user(userId)) doInDirectChat(u, operation); else qCCritical(MAIN) << "Connection::doInDirectChat: Couldn't get a user object for" << userId; } void Connection::doInDirectChat(User* u, const std::function& operation) { Q_ASSERT(u); const auto& otherUserId = u->id(); // There can be more than one DC; find the first valid (existing and // not left), and delete inexistent (forgotten?) ones along the way. DirectChatsMap removals; for (auto it = d->directChats.find(u); it != d->directChats.end() && it.key() == u; ++it) { const auto& roomId = *it; if (auto r = room(roomId, JoinState::Join)) { Q_ASSERT(r->id() == roomId); // A direct chat with yourself should only involve yourself :) if (otherUserId == userId() && r->totalMemberCount() > 1) continue; qCDebug(MAIN) << "Requested direct chat with" << otherUserId << "is already available as" << r->id(); operation(r); return; } if (auto ir = invitation(roomId)) { Q_ASSERT(ir->id() == roomId); auto j = joinRoom(ir->id()); connect(j, &BaseJob::success, this, [this, roomId, otherUserId, operation] { qCDebug(MAIN) << "Joined the already invited direct chat with" << otherUserId << "as" << roomId; operation(room(roomId, JoinState::Join)); }); return; } // Avoid reusing previously left chats but don't remove them // from direct chat maps, either. if (room(roomId, JoinState::Leave)) continue; qCWarning(MAIN) << "Direct chat with" << otherUserId << "known as room" << roomId << "is not valid and will be discarded"; // Postpone actual deletion until we finish iterating d->directChats. removals.insert(it.key(), it.value()); // Add to the list of updates to send to the server upon the next sync. d->dcLocalRemovals.insert(it.key(), it.value()); } if (!removals.isEmpty()) { for (auto it = removals.cbegin(); it != removals.cend(); ++it) { d->directChats.remove(it.key(), it.value()); d->directChatUsers.remove(it.value(), const_cast(it.key())); // FIXME } emit directChatsListChanged({}, removals); } auto j = createDirectChat(otherUserId); connect(j, &BaseJob::success, this, [this, j, otherUserId, operation] { qCDebug(MAIN) << "Direct chat with" << otherUserId << "has been created as" << j->roomId(); operation(room(j->roomId(), JoinState::Join)); }); } CreateRoomJob* Connection::createDirectChat(const QString& userId, const QString& topic, const QString& name) { return createRoom(UnpublishRoom, {}, name, topic, { userId }, QStringLiteral("trusted_private_chat"), {}, true); } ForgetRoomJob* Connection::forgetRoom(const QString& id) { // To forget is hard :) First we should ensure the local user is not // in the room (by leaving it, if necessary); once it's done, the /forget // endpoint can be called; and once this is through, the local Room object // (if any existed) is deleted. At the same time, we still have to // (basically immediately) return a pointer to ForgetRoomJob. Therefore // a ForgetRoomJob is created in advance and can be returned in a probably // not-yet-started state (it will start once /leave completes). auto forgetJob = new ForgetRoomJob(id); auto room = d->roomMap.value({ id, false }); if (!room) room = d->roomMap.value({ id, true }); if (room && room->joinState() != JoinState::Leave) { auto leaveJob = room->leaveRoom(); connect(leaveJob, &BaseJob::result, this, [this, leaveJob, forgetJob, room] { if (leaveJob->error() == BaseJob::Success || leaveJob->error() == BaseJob::NotFoundError) { run(forgetJob); // If the matching /sync response hasn't arrived yet, // mark the room for explicit deletion if (room->joinState() != JoinState::Leave) d->roomIdsToForget.push_back(room->id()); } else { qCWarning(MAIN).nospace() << "Error leaving room " << room->objectName() << ": " << leaveJob->errorString(); forgetJob->abandon(); } }); connect(leaveJob, &BaseJob::failure, forgetJob, &BaseJob::abandon); } else run(forgetJob); connect(forgetJob, &BaseJob::result, this, [this, id, forgetJob] { // Leave room in case of success, or room not known by server if (forgetJob->error() == BaseJob::Success || forgetJob->error() == BaseJob::NotFoundError) d->removeRoom(id); // Delete the room from roomMap else qCWarning(MAIN).nospace() << "Error forgetting room " << id << ": " << forgetJob->errorString(); }); return forgetJob; } SendToDeviceJob* Connection::sendToDevices(const QString& eventType, const UsersToDevicesToEvents& eventsMap) const { QHash> json; json.reserve(int(eventsMap.size())); std::for_each(eventsMap.begin(), eventsMap.end(), [&json](const auto& userTodevicesToEvents) { auto& jsonUser = json[userTodevicesToEvents.first]; const auto& devicesToEvents = userTodevicesToEvents.second; std::for_each(devicesToEvents.begin(), devicesToEvents.end(), [&jsonUser](const auto& deviceToEvents) { jsonUser.insert( deviceToEvents.first, deviceToEvents.second.contentJson()); }); }); return callApi(BackgroundRequest, eventType, generateTxnId(), json); } SendMessageJob* Connection::sendMessage(const QString& roomId, const RoomEvent& event) const { const auto txnId = event.transactionId().isEmpty() ? generateTxnId() : event.transactionId(); return callApi(roomId, event.matrixType(), txnId, event.contentJson()); } QUrl Connection::homeserver() const { return d->data->baseUrl(); } QString Connection::domain() const { return userId().section(':', 1); } Room* Connection::room(const QString& roomId, JoinStates states) const { Room* room = d->roomMap.value({ roomId, false }, nullptr); if (states.testFlag(JoinState::Join) && room && room->joinState() == JoinState::Join) return room; if (states.testFlag(JoinState::Invite)) if (Room* invRoom = invitation(roomId)) return invRoom; if (states.testFlag(JoinState::Leave) && room && room->joinState() == JoinState::Leave) return room; return nullptr; } Room* Connection::roomByAlias(const QString& roomAlias, JoinStates states) const { const auto id = d->roomAliasMap.value(serverPart(roomAlias)).value(roomAlias); if (!id.isEmpty()) return room(id, states); qCWarning(MAIN) << "Room for alias" << roomAlias << "is not found under account" << userId(); return nullptr; } void Connection::updateRoomAliases(const QString& roomId, const QString& aliasServer, const QStringList& previousRoomAliases, const QStringList& roomAliases) { auto& aliasMap = d->roomAliasMap[aliasServer]; // Allocate if necessary for (const auto& a : previousRoomAliases) if (aliasMap.remove(a) == 0) qCWarning(MAIN) << "Alias" << a << "is not found (already deleted?)"; for (const auto& a : roomAliases) { auto& mappedId = aliasMap[a]; if (!mappedId.isEmpty()) { if (mappedId == roomId) qCDebug(MAIN) << "Alias" << a << "is already mapped to" << roomId; else qCWarning(MAIN) << "Alias" << a << "will be force-remapped from" << mappedId << "to" << roomId; } mappedId = roomId; } } Room* Connection::invitation(const QString& roomId) const { return d->roomMap.value({ roomId, true }, nullptr); } User* Connection::user(const QString& uId) { if (uId.isEmpty()) return nullptr; if (!uId.startsWith('@') || !uId.contains(':')) { qCCritical(MAIN) << "Malformed userId:" << uId; return nullptr; } if (d->userMap.contains(uId)) return d->userMap.value(uId); auto* user = userFactory()(this, uId); d->userMap.insert(uId, user); emit newUser(user); return user; } const User* Connection::user() const { return d->userMap.value(userId(), nullptr); } User* Connection::user() { return user(userId()); } QString Connection::userId() const { return d->data->userId(); } QString Connection::deviceId() const { return d->data->deviceId(); } QByteArray Connection::accessToken() const { return d->data->accessToken(); } QtOlm::Account* Connection::olmAccount() const { return d->encryptionManager->account(); } SyncJob* Connection::syncJob() const { return d->syncJob; } int Connection::millisToReconnect() const { return d->syncJob ? d->syncJob->millisToRetry() : 0; } QHash, Room*> Connection::roomMap() const { // Copy-on-write-and-remove-elements is faster than copying elements one by // one. QHash, Room*> roomMap = d->roomMap; for (auto it = roomMap.begin(); it != roomMap.end();) { if (it.value()->joinState() == JoinState::Leave) it = roomMap.erase(it); else ++it; } return roomMap; } QVector Connection::allRooms() const { QVector result; result.resize(d->roomMap.size()); std::copy(d->roomMap.cbegin(), d->roomMap.cend(), result.begin()); return result; } QVector Connection::rooms(JoinStates joinStates) const { QVector result; for (auto* r: qAsConst(d->roomMap)) if (joinStates.testFlag(r->joinState())) result.push_back(r); return result; } int Connection::roomsCount(JoinStates joinStates) const { // Using int to maintain compatibility with QML // (consider also that QHash<>::size() returns int anyway). return int(std::count_if(d->roomMap.begin(), d->roomMap.end(), [joinStates](Room* r) { return joinStates.testFlag(r->joinState()); })); } bool Connection::hasAccountData(const QString& type) const { return d->accountData.find(type) != d->accountData.cend(); } const EventPtr& Connection::accountData(const QString& type) const { static EventPtr NoEventPtr {}; auto it = d->accountData.find(type); return it == d->accountData.end() ? NoEventPtr : it->second; } QJsonObject Connection::accountDataJson(const QString& type) const { const auto& eventPtr = accountData(type); return eventPtr ? eventPtr->contentJson() : QJsonObject(); } void Connection::setAccountData(EventPtr&& event) { d->packAndSendAccountData(std::move(event)); } void Connection::setAccountData(const QString& type, const QJsonObject& content) { d->packAndSendAccountData(loadEvent(type, content)); } QHash> Connection::tagsToRooms() const { QHash> result; for (auto* r : qAsConst(d->roomMap)) { const auto& tagNames = r->tagNames(); for (const auto& tagName : tagNames) result[tagName].push_back(r); } for (auto it = result.begin(); it != result.end(); ++it) std::sort(it->begin(), it->end(), [t = it.key()](Room* r1, Room* r2) { return r1->tags().value(t) < r2->tags().value(t); }); return result; } QStringList Connection::tagNames() const { QStringList tags({ FavouriteTag }); for (auto* r : qAsConst(d->roomMap)) { const auto& tagNames = r->tagNames(); for (const auto& tag : tagNames) if (tag != LowPriorityTag && !tags.contains(tag)) tags.push_back(tag); } tags.push_back(LowPriorityTag); return tags; } QVector Connection::roomsWithTag(const QString& tagName) const { QVector rooms; std::copy_if(d->roomMap.begin(), d->roomMap.end(), std::back_inserter(rooms), [&tagName](Room* r) { return r->tags().contains(tagName); }); return rooms; } Connection::DirectChatsMap Connection::directChats() const { return d->directChats; } // Removes room with given id from roomMap void Connection::Private::removeRoom(const QString& roomId) { for (auto f : { false, true }) if (auto r = roomMap.take({ roomId, f })) { qCDebug(MAIN) << "Room" << r->objectName() << "in state" << toCString(r->joinState()) << "will be deleted"; emit r->beforeDestruction(r); r->deleteLater(); } } void Connection::addToDirectChats(const Room* room, User* user) { Q_ASSERT(room != nullptr && user != nullptr); if (d->directChats.contains(user, room->id())) return; Q_ASSERT(!d->directChatUsers.contains(room->id(), user)); d->directChats.insert(user, room->id()); d->directChatUsers.insert(room->id(), user); d->dcLocalAdditions.insert(user, room->id()); emit directChatsListChanged({ { user, room->id() } }, {}); } void Connection::removeFromDirectChats(const QString& roomId, User* user) { Q_ASSERT(!roomId.isEmpty()); if ((user != nullptr && !d->directChats.contains(user, roomId)) || d->directChats.key(roomId) == nullptr) return; DirectChatsMap removals; if (user != nullptr) { d->directChats.remove(user, roomId); d->directChatUsers.remove(roomId, user); removals.insert(user, roomId); d->dcLocalRemovals.insert(user, roomId); } else { removals = erase_if(d->directChats, [&roomId](auto it) { return it.value() == roomId; }); d->directChatUsers.remove(roomId); d->dcLocalRemovals += removals; } emit directChatsListChanged({}, removals); } bool Connection::isDirectChat(const QString& roomId) const { return d->directChatUsers.contains(roomId); } QList Connection::directChatUsers(const Room* room) const { Q_ASSERT(room != nullptr); return d->directChatUsers.values(room->id()); } bool Connection::isIgnored(const User* user) const { return ignoredUsers().contains(user->id()); } Connection::IgnoredUsersList Connection::ignoredUsers() const { const auto* event = d->unpackAccountData(); return event ? event->ignored_users() : IgnoredUsersList(); } void Connection::addToIgnoredUsers(const User* user) { Q_ASSERT(user != nullptr); auto ignoreList = ignoredUsers(); if (!ignoreList.contains(user->id())) { ignoreList.insert(user->id()); d->packAndSendAccountData(ignoreList); emit ignoredUsersListChanged({ { user->id() } }, {}); } } void Connection::removeFromIgnoredUsers(const User* user) { Q_ASSERT(user != nullptr); auto ignoreList = ignoredUsers(); if (ignoreList.remove(user->id()) != 0) { d->packAndSendAccountData(ignoreList); emit ignoredUsersListChanged({}, { { user->id() } }); } } QMap Connection::users() const { return d->userMap; } const ConnectionData* Connection::connectionData() const { return d->data.get(); } Room* Connection::provideRoom(const QString& id, Omittable joinState) { // TODO: This whole function is a strong case for a RoomManager class. Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id"); // If joinState.omitted(), all joinState == comparisons below are false. const auto roomKey = qMakePair(id, joinState == JoinState::Invite); auto* room = d->roomMap.value(roomKey, nullptr); if (room) { // Leave is a special case because in transition (5a) (see the .h file) // joinState == room->joinState but we still have to preempt the Invite // and emit a signal. For Invite and Join, there's no such problem. if (room->joinState() == joinState && joinState != JoinState::Leave) return room; } else if (joinState.omitted()) { // No Join and Leave, maybe Invite? room = d->roomMap.value({ id, true }, nullptr); if (room) return room; // No Invite either, setup a new room object below } if (!room) { room = roomFactory()(this, id, joinState.omitted() ? JoinState::Join : joinState.value()); if (!room) { qCCritical(MAIN) << "Failed to create a room" << id; return nullptr; } d->roomMap.insert(roomKey, room); d->firstTimeRooms.push_back(room); connect(room, &Room::beforeDestruction, this, &Connection::aboutToDeleteRoom); emit newRoom(room); } if (joinState.omitted()) return room; if (joinState == JoinState::Invite) { // prev is either Leave or nullptr auto* prev = d->roomMap.value({ id, false }, nullptr); emit invitedRoom(room, prev); } else { room->setJoinState(joinState.value()); // Preempt the Invite room (if any) with a room in Join/Leave state. auto* prevInvite = d->roomMap.take({ id, true }); if (joinState == JoinState::Join) emit joinedRoom(room, prevInvite); else if (joinState == JoinState::Leave) emit leftRoom(room, prevInvite); if (prevInvite) { const auto dcUsers = prevInvite->directChatUsers(); for (auto* u : dcUsers) addToDirectChats(room, u); qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id(); emit prevInvite->beforeDestruction(prevInvite); prevInvite->deleteLater(); } } return room; } void Connection::setRoomFactory(room_factory_t f) { _roomFactory = std::move(f); } void Connection::setUserFactory(user_factory_t f) { _userFactory = std::move(f); } room_factory_t Connection::roomFactory() { return _roomFactory; } user_factory_t Connection::userFactory() { return _userFactory; } room_factory_t Connection::_roomFactory = defaultRoomFactory<>(); user_factory_t Connection::_userFactory = defaultUserFactory<>(); QByteArray Connection::generateTxnId() const { return d->data->generateTxnId(); } void Connection::setHomeserver(const QUrl& url) { if (homeserver() == url) return; d->data->setBaseUrl(url); emit homeserverChanged(homeserver()); } void Connection::saveRoomState(Room* r) const { Q_ASSERT(r); if (!d->cacheState) return; QFile outRoomFile { stateCacheDir().filePath( SyncData::fileNameForRoom(r->id())) }; if (outRoomFile.open(QFile::WriteOnly)) { QJsonDocument json { r->toJson() }; auto data = d->cacheToBinary ? json.toBinaryData() : json.toJson(QJsonDocument::Compact); outRoomFile.write(data.data(), data.size()); qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName(); } else { qCWarning(MAIN) << "Error opening" << outRoomFile.fileName() << ":" << outRoomFile.errorString(); } } void Connection::saveState() const { if (!d->cacheState) return; QElapsedTimer et; et.start(); QFile outFile { d->topLevelStatePath() }; if (!outFile.open(QFile::WriteOnly)) { qCWarning(MAIN) << "Error opening" << outFile.fileName() << ":" << outFile.errorString(); qCWarning(MAIN) << "Caching the rooms state disabled"; d->cacheState = false; return; } QJsonObject rootObj { { QStringLiteral("cache_version"), QJsonObject { { QStringLiteral("major"), SyncData::cacheVersion().first }, { QStringLiteral("minor"), SyncData::cacheVersion().second } } } }; { QJsonObject roomsJson; QJsonObject inviteRoomsJson; for (const auto* r: qAsConst(d->roomMap)) { if (r->joinState() == JoinState::Leave) continue; (r->joinState() == JoinState::Invite ? inviteRoomsJson : roomsJson) .insert(r->id(), QJsonValue::Null); } QJsonObject roomObj; if (!roomsJson.isEmpty()) roomObj.insert(QStringLiteral("join"), roomsJson); if (!inviteRoomsJson.isEmpty()) roomObj.insert(QStringLiteral("invite"), inviteRoomsJson); rootObj.insert(QStringLiteral("next_batch"), d->data->lastEvent()); rootObj.insert(QStringLiteral("rooms"), roomObj); } { QJsonArray accountDataEvents { basicEventJson(QStringLiteral("m.direct"), toJson(d->directChats)) }; for (const auto& e : d->accountData) accountDataEvents.append( basicEventJson(e.first, e.second->contentJson())); rootObj.insert(QStringLiteral("account_data"), QJsonObject { { QStringLiteral("events"), accountDataEvents } }); } QJsonDocument json { rootObj }; auto data = d->cacheToBinary ? json.toBinaryData() : json.toJson(QJsonDocument::Compact); qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; outFile.write(data.data(), data.size()); qCDebug(MAIN) << "State cache saved to" << outFile.fileName(); } void Connection::loadState() { if (!d->cacheState) return; QElapsedTimer et; et.start(); SyncData sync { d->topLevelStatePath() }; if (sync.nextBatch().isEmpty()) // No token means no cache by definition return; if (!sync.unresolvedRooms().isEmpty()) { qCWarning(MAIN) << "State cache incomplete, discarding"; return; } // TODO: to handle load failures, instead of the above block: // 1. Do initial sync on failed rooms without saving the nextBatch token // 2. Do the sync across all rooms as normal onSyncSuccess(std::move(sync), true); qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et; } QString Connection::stateCachePath() const { return stateCacheDir().path() % '/'; } QDir Connection::stateCacheDir() const { auto safeUserId = userId(); safeUserId.replace(':', '_'); return cacheLocation(safeUserId); } bool Connection::cacheState() const { return d->cacheState; } void Connection::setCacheState(bool newValue) { if (d->cacheState != newValue) { d->cacheState = newValue; emit cacheStateChanged(); } } bool Connection::lazyLoading() const { return d->lazyLoading; } void Connection::setLazyLoading(bool newValue) { if (d->lazyLoading != newValue) { d->lazyLoading = newValue; emit lazyLoadingChanged(); } } void Connection::run(BaseJob* job, RunningPolicy runningPolicy) const { connect(job, &BaseJob::failure, this, &Connection::requestFailed); job->prepare(d->data.get(), runningPolicy & BackgroundRequest); d->data->submit(job); } void Connection::getTurnServers() { auto job = callApi(); connect(job, &GetTurnServerJob::success, this, [=] { emit turnServersChanged(job->data()); }); } const QString Connection::SupportedRoomVersion::StableTag = QStringLiteral("stable"); QString Connection::defaultRoomVersion() const { Q_ASSERT(!d->capabilities.roomVersions.omitted()); return d->capabilities.roomVersions->defaultVersion; } QStringList Connection::stableRoomVersions() const { Q_ASSERT(!d->capabilities.roomVersions.omitted()); QStringList l; const auto& allVersions = d->capabilities.roomVersions->available; for (auto it = allVersions.begin(); it != allVersions.end(); ++it) if (it.value() == SupportedRoomVersion::StableTag) l.push_back(it.key()); return l; } inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1, const Connection::SupportedRoomVersion& v2) { bool ok1 = false, ok2 = false; const auto vNum1 = v1.id.toFloat(&ok1); const auto vNum2 = v2.id.toFloat(&ok2); return ok1 && ok2 ? vNum1 < vNum2 : v1.id < v2.id; } QVector Connection::availableRoomVersions() const { Q_ASSERT(!d->capabilities.roomVersions.omitted()); QVector result; result.reserve(d->capabilities.roomVersions->available.size()); for (auto it = d->capabilities.roomVersions->available.begin(); it != d->capabilities.roomVersions->available.end(); ++it) result.push_back({ it.key(), it.value() }); // Put stable versions over unstable; within each group, // sort numeric versions as numbers, the rest as strings. const auto mid = std::partition(result.begin(), result.end(), std::mem_fn(&SupportedRoomVersion::isStable)); std::sort(result.begin(), mid, roomVersionLess); std::sort(mid, result.end(), roomVersionLess); return result; } spectral/include/libQuotient/lib/converters.h0000644000175000000620000003235313566674122021425 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "util.h" #include #include // Includes #include #include #include #include #include #include // Enable std::unordered_map // REMOVEME in favor of UnorderedMap, once we regenerate API files namespace std { template <> struct hash { size_t operator()(const QString& s) const Q_DECL_NOEXCEPT { return qHash(s #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) , uint(qGlobalQHashSeed()) #endif ); } }; } // namespace std class QVariant; namespace Quotient { template struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod.toJson(); } static void fillFrom(const QJsonObject& jo, T& pod) { pod = T(jo); } }; template struct JsonConverter { static QJsonObject dump(const T& pod) { QJsonObject jo; JsonObjectConverter::dumpTo(jo, pod); return jo; } static T doLoad(const QJsonObject& jo) { T pod; JsonObjectConverter::fillFrom(jo, pod); return pod; } static T load(const QJsonValue& jv) { return doLoad(jv.toObject()); } static T load(const QJsonDocument& jd) { return doLoad(jd.object()); } }; template inline auto toJson(const T& pod) { return JsonConverter::dump(pod); } inline auto toJson(const QJsonObject& jo) { return jo; } template inline void fillJson(QJsonObject& json, const T& data) { JsonObjectConverter::dumpTo(json, data); } template inline T fromJson(const QJsonValue& jv) { return JsonConverter::load(jv); } template inline T fromJson(const QJsonDocument& jd) { return JsonConverter::load(jd); } // Convenience fromJson() overloads that deduce T instead of requiring // the coder to explicitly type it. They still enforce the // overwrite-everything semantics of fromJson(), unlike fillFromJson() template inline void fromJson(const QJsonValue& jv, T& pod) { pod = jv.isUndefined() ? T() : fromJson(jv); } template inline void fromJson(const QJsonDocument& jd, T& pod) { pod = fromJson(jd); } template inline void fillFromJson(const QJsonValue& jv, T& pod) { if (jv.isObject()) JsonObjectConverter::fillFrom(jv.toObject(), pod); else if (!jv.isUndefined()) pod = fromJson(jv); } // JsonConverter<> specialisations template struct TrivialJsonDumper { // Works for: QJsonValue (and all things it can consume), // QJsonObject, QJsonArray static auto dump(const T& val) { return val; } }; template <> struct JsonConverter : public TrivialJsonDumper { static auto load(const QJsonValue& jv) { return jv.toBool(); } }; template <> struct JsonConverter : public TrivialJsonDumper { static auto load(const QJsonValue& jv) { return jv.toInt(); } }; template <> struct JsonConverter : public TrivialJsonDumper { static auto load(const QJsonValue& jv) { return jv.toDouble(); } }; template <> struct JsonConverter : public TrivialJsonDumper { static auto load(const QJsonValue& jv) { return float(jv.toDouble()); } }; template <> struct JsonConverter : public TrivialJsonDumper { static auto load(const QJsonValue& jv) { return qint64(jv.toDouble()); } }; template <> struct JsonConverter : public TrivialJsonDumper { static auto load(const QJsonValue& jv) { return jv.toString(); } }; template <> struct JsonConverter { static auto dump(const QDateTime& val) = delete; // not provided yet static auto load(const QJsonValue& jv) { return QDateTime::fromMSecsSinceEpoch(fromJson(jv), Qt::UTC); } }; template <> struct JsonConverter { static auto dump(const QDate& val) = delete; // not provided yet static auto load(const QJsonValue& jv) { return fromJson(jv).date(); } }; template <> struct JsonConverter : public TrivialJsonDumper { static auto load(const QJsonValue& jv) { return jv.toArray(); } }; template <> struct JsonConverter { static QString dump(const QByteArray& ba) { return ba.constData(); } static auto load(const QJsonValue& jv) { return fromJson(jv).toLatin1(); } }; template <> struct JsonConverter { static QJsonValue dump(const QVariant& v); static QVariant load(const QJsonValue& jv); }; template struct JsonConverter> { static QJsonValue dump(const Omittable& from) { return from.omitted() ? QJsonValue() : toJson(from.value()); } static Omittable load(const QJsonValue& jv) { if (jv.isUndefined()) return none; return fromJson(jv); } }; template struct JsonArrayConverter { static void dumpTo(QJsonArray& ar, const VectorT& vals) { for (const auto& v : vals) ar.push_back(toJson(v)); } static auto dump(const VectorT& vals) { QJsonArray ja; dumpTo(ja, vals); return ja; } static auto load(const QJsonArray& ja) { VectorT vect; vect.reserve(typename VectorT::size_type(ja.size())); for (const auto& i : ja) vect.push_back(fromJson(i)); return vect; } static auto load(const QJsonValue& jv) { return load(jv.toArray()); } static auto load(const QJsonDocument& jd) { return load(jd.array()); } }; template struct JsonConverter> : public JsonArrayConverter> {}; template struct JsonConverter> : public JsonArrayConverter> {}; template struct JsonConverter> : public JsonArrayConverter> {}; template <> struct JsonConverter : public JsonConverter> { static auto dump(const QStringList& sl) { return QJsonArray::fromStringList(sl); } }; template <> struct JsonObjectConverter> { static void dumpTo(QJsonObject& json, const QSet& s) { for (const auto& e : s) json.insert(toJson(e), QJsonObject {}); } static auto fillFrom(const QJsonObject& json, QSet& s) { s.reserve(s.size() + json.size()); for (auto it = json.begin(); it != json.end(); ++it) s.insert(it.key()); return s; } }; template struct HashMapFromJson { static void dumpTo(QJsonObject& json, const HashMapT& hashMap) { for (auto it = hashMap.begin(); it != hashMap.end(); ++it) json.insert(it.key(), toJson(it.value())); } static void fillFrom(const QJsonObject& jo, HashMapT& h) { h.reserve(jo.size()); for (auto it = jo.begin(); it != jo.end(); ++it) h[it.key()] = fromJson(it.value()); } }; template struct JsonObjectConverter> : public HashMapFromJson> {}; template struct JsonObjectConverter> : public HashMapFromJson> {}; // We could use std::conditional<> below but QT_VERSION* macros in C++ code // cause (kinda valid but useless and noisy) compiler warnings about // bitwise operations on signed integers; so use the preprocessor for now. using variant_map_t = #if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)) QVariantHash; #else QVariantMap; #endif template <> struct JsonConverter { static QJsonObject dump(const variant_map_t& vh); static QVariantHash load(const QJsonValue& jv); }; // Conditional insertion into a QJsonObject namespace _impl { template inline void addTo(QJsonObject& o, const QString& k, ValT&& v) { o.insert(k, toJson(v)); } template inline void addTo(QUrlQuery& q, const QString& k, ValT&& v) { q.addQueryItem(k, QStringLiteral("%1").arg(v)); } // OpenAPI is entirely JSON-based, which means representing bools as // textual true/false, rather than 1/0. inline void addTo(QUrlQuery& q, const QString& k, bool v) { q.addQueryItem(k, v ? QStringLiteral("true") : QStringLiteral("false")); } inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals) { for (const auto& v : vals) q.addQueryItem(k, v); } inline void addTo(QUrlQuery& q, const QString&, const QJsonObject& vals) { for (auto it = vals.begin(); it != vals.end(); ++it) q.addQueryItem(it.key(), it.value().toString()); } // This one is for types that don't have isEmpty() and for all types // when Force is true template struct AddNode { template static void impl(ContT& container, const QString& key, ForwardedT&& value) { addTo(container, key, std::forward(value)); } }; // This one is for types that have isEmpty() when Force is false template struct AddNode().isEmpty())> { template static void impl(ContT& container, const QString& key, ForwardedT&& value) { if (!value.isEmpty()) addTo(container, key, std::forward(value)); } }; // This one unfolds Omittable<> (also only when Force is false) template struct AddNode, false> { template static void impl(ContT& container, const QString& key, const OmittableT& value) { if (!value.omitted()) addTo(container, key, value.value()); } }; #if 0 // This is a special one that unfolds optional<> template struct AddNode, Force> { template static void impl(ContT& container, const QString& key, const OptionalT& value) { if (value) AddNode::impl(container, key, value.value()); else if (Force) // Edge case, no value but must put something AddNode::impl(container, key, QString{}); } }; #endif } // namespace _impl static constexpr bool IfNotEmpty = false; /*! Add a key-value pair to QJsonObject or QUrlQuery * * Adds a key-value pair(s) specified by \p key and \p value to * \p container, optionally (in case IfNotEmpty is passed for the first * template parameter) taking into account the value "emptiness". * With IfNotEmpty, \p value is NOT added to the container if and only if: * - it has a method `isEmpty()` and `value.isEmpty() == true`, or * - it's an `Omittable<>` and `value.omitted() == true`. * * If \p container is a QUrlQuery, an attempt to fit \p value into it is * made as follows: * - if \p value is a QJsonObject, \p key is ignored and pairs from \p value * are copied to \p container, assuming that the value in each pair * is a string; * - if \p value is a QStringList, it is "exploded" into a list of key-value * pairs with key equal to \p key and value taken from each list item; * - if \p value is a bool, its OpenAPI (i.e. JSON) representation is added * to the query (`true` or `false`, respectively). * * \tparam Force add the pair even if the value is empty. This is true * by default; passing IfNotEmpty or false for this parameter * enables emptiness checks as described above */ template inline void addParam(ContT& container, const QString& key, ValT&& value) { _impl::AddNode, Force>::impl(container, key, std::forward(value)); } } // namespace Quotient spectral/include/libQuotient/lib/jobs/0002755000175000000620000000000013566674122020013 5ustar dilingerstaffspectral/include/libQuotient/lib/jobs/basejob.cpp0000644000175000000620000005371113566674122022131 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "basejob.h" #include "connectiondata.h" #include "util.h" #include #include #include #include #include #include #include #include using namespace Quotient; using std::chrono::seconds, std::chrono::milliseconds; using namespace std::chrono_literals; struct NetworkReplyDeleter : public QScopedPointerDeleteLater { static inline void cleanup(QNetworkReply* reply) { if (reply && reply->isRunning()) reply->abort(); QScopedPointerDeleteLater::cleanup(reply); } }; template constexpr auto make_array(Ts&&... items) { return std::array, sizeof...(Ts)>({items...}); } class BaseJob::Private { public: struct JobTimeoutConfig { seconds jobTimeout; seconds nextRetryInterval; }; // Using an idiom from clang-tidy: // http://clang.llvm.org/extra/clang-tidy/checks/modernize-pass-by-value.html Private(HttpVerb v, QString endpoint, const QUrlQuery& q, Data&& data, bool nt) : verb(v) , apiEndpoint(std::move(endpoint)) , requestQuery(q) , requestData(std::move(data)) , needsToken(nt) { timer.setSingleShot(true); retryTimer.setSingleShot(true); } void sendRequest(); ConnectionData* connection = nullptr; // Contents for the network request HttpVerb verb; QString apiEndpoint; QHash requestHeaders; QUrlQuery requestQuery; Data requestData; bool needsToken; bool inBackground = false; // There's no use of QMimeType here because we don't want to match // content types against the known MIME type hierarchy; and at the same // type QMimeType is of little help with MIME type globs (`text/*` etc.) QByteArrayList expectedContentTypes { "application/json" }; QScopedPointer reply; Status status = Unprepared; QByteArray rawResponse; QUrl errorUrl; //< May contain a URL to help with some errors LoggingCategory logCat = JOBS; QTimer timer; QTimer retryTimer; static constexpr std::array errorStrategy { { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } } }; int maxRetries = int(errorStrategy.size()); int retriesTaken = 0; [[nodiscard]] const JobTimeoutConfig& getCurrentTimeoutConfig() const { return errorStrategy[std::min(size_t(retriesTaken), errorStrategy.size() - 1)]; } [[nodiscard]] QString dumpRequest() const { // FIXME: use std::array {} when Apple stdlib gets deduction guides for it static const auto verbs = make_array(QStringLiteral("GET"), QStringLiteral("PUT"), QStringLiteral("POST"), QStringLiteral("DELETE")); const auto verbWord = verbs.at(size_t(verb)); return verbWord % ' ' % (reply ? reply->url().toString(QUrl::RemoveQuery) : makeRequestUrl(connection->baseUrl(), apiEndpoint) .toString()); } }; BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, bool needsToken) : BaseJob(verb, name, endpoint, Query {}, Data {}, needsToken) {} BaseJob::BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, const Query& query, Data&& data, bool needsToken) : d(new Private(verb, endpoint, query, std::move(data), needsToken)) { setObjectName(name); connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout); connect(&d->retryTimer, &QTimer::timeout, this, [this] { setStatus(Pending); sendRequest(); }); } BaseJob::~BaseJob() { stop(); qCDebug(d->logCat) << this << "destroyed"; } QUrl BaseJob::requestUrl() const { return d->reply ? d->reply->url() : QUrl(); } bool BaseJob::isBackground() const { return d->inBackground; } const QString& BaseJob::apiEndpoint() const { return d->apiEndpoint; } void BaseJob::setApiEndpoint(const QString& apiEndpoint) { d->apiEndpoint = apiEndpoint; } const BaseJob::headers_t& BaseJob::requestHeaders() const { return d->requestHeaders; } void BaseJob::setRequestHeader(const headers_t::key_type& headerName, const headers_t::mapped_type& headerValue) { d->requestHeaders[headerName] = headerValue; } void BaseJob::setRequestHeaders(const BaseJob::headers_t& headers) { d->requestHeaders = headers; } const QUrlQuery& BaseJob::query() const { return d->requestQuery; } void BaseJob::setRequestQuery(const QUrlQuery& query) { d->requestQuery = query; } const BaseJob::Data& BaseJob::requestData() const { return d->requestData; } void BaseJob::setRequestData(Data&& data) { std::swap(d->requestData, data); } const QByteArrayList& BaseJob::expectedContentTypes() const { return d->expectedContentTypes; } void BaseJob::addExpectedContentType(const QByteArray& contentType) { d->expectedContentTypes << contentType; } void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes) { d->expectedContentTypes = contentTypes; } QUrl BaseJob::makeRequestUrl(QUrl baseUrl, const QString& path, const QUrlQuery& query) { auto pathBase = baseUrl.path(); if (!pathBase.endsWith('/') && !path.startsWith('/')) pathBase.push_back('/'); baseUrl.setPath(pathBase + path, QUrl::TolerantMode); baseUrl.setQuery(query); return baseUrl; } void BaseJob::Private::sendRequest() { QNetworkRequest req { makeRequestUrl(connection->baseUrl(), apiEndpoint, requestQuery) }; if (!requestHeaders.contains("Content-Type")) req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); req.setRawHeader("Authorization", QByteArray("Bearer ") + connection->accessToken()); req.setAttribute(QNetworkRequest::BackgroundRequestAttribute, inBackground); req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); req.setMaximumRedirectsAllowed(10); req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true); req.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true); for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it) req.setRawHeader(it.key(), it.value()); switch (verb) { case HttpVerb::Get: reply.reset(connection->nam()->get(req)); break; case HttpVerb::Post: reply.reset(connection->nam()->post(req, requestData.source())); break; case HttpVerb::Put: reply.reset(connection->nam()->put(req, requestData.source())); break; case HttpVerb::Delete: reply.reset(connection->nam()->deleteResource(req)); break; } } void BaseJob::doPrepare() {} void BaseJob::onSentRequest(QNetworkReply*) {} void BaseJob::beforeAbandon(QNetworkReply*) {} void BaseJob::prepare(ConnectionData* connData, bool inBackground) { d->inBackground = inBackground; d->connection = connData; doPrepare(); if (status().code != Unprepared && status().code != Pending) QTimer::singleShot(0, this, &BaseJob::finishJob); setStatus(Pending); } void BaseJob::sendRequest() { if (status().code == Abandoned) return; Q_ASSERT(d->connection && status().code == Pending); qCDebug(d->logCat).noquote() << "Making" << d->dumpRequest(); emit aboutToSendRequest(); d->sendRequest(); Q_ASSERT(d->reply); connect(d->reply.data(), &QNetworkReply::finished, this, &BaseJob::gotReply); if (d->reply->isRunning()) { connect(d->reply.data(), &QNetworkReply::metaDataChanged, this, &BaseJob::checkReply); connect(d->reply.data(), &QNetworkReply::uploadProgress, this, &BaseJob::uploadProgress); connect(d->reply.data(), &QNetworkReply::downloadProgress, this, &BaseJob::downloadProgress); d->timer.start(getCurrentTimeout()); qCInfo(d->logCat).noquote() << "Sent" << d->dumpRequest(); onSentRequest(d->reply.data()); emit sentRequest(); } else qCWarning(d->logCat).noquote() << "Request could not start:" << d->dumpRequest(); } void BaseJob::checkReply() { setStatus(doCheckReply(d->reply.data())); } void BaseJob::gotReply() { checkReply(); if (status().good()) setStatus(parseReply(d->reply.data())); else { d->rawResponse = d->reply->readAll(); const auto jsonBody = d->reply->rawHeader("Content-Type") == "application/json"; qCDebug(d->logCat).noquote() << "Error body (truncated if long):" << d->rawResponse.left(500); if (jsonBody) setStatus( parseError(d->reply.data(), QJsonDocument::fromJson(d->rawResponse).object())); } finishJob(); } bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) { if (patterns.isEmpty()) return true; // ignore possible appendixes of the content type const auto ctype = type.split(';').front(); for (const auto& pattern : patterns) { if (pattern.startsWith('*') || ctype == pattern) // Fast lane return true; auto patternParts = pattern.split('/'); Q_ASSERT_X(patternParts.size() <= 2, __FUNCTION__, "BaseJob: Expected content type should have up to two" " /-separated parts; violating pattern: " + pattern); if (ctype.split('/').front() == patternParts.front() && patternParts.back() == "*") return true; // Exact match already went on fast lane } return false; } BaseJob::Status BaseJob::Status::fromHttpCode(int httpCode, QString msg) { // clang-format off return { [httpCode]() -> StatusCode { if (httpCode / 10 == 41) // 41x errors return httpCode == 410 ? IncorrectRequestError : NotFoundError; switch (httpCode) { case 401: case 403: case 407: return ContentAccessError; case 404: return NotFoundError; case 400: case 405: case 406: case 426: case 428: case 505: case 494: // Unofficial nginx "Request header too large" case 497: // Unofficial nginx "HTTP request sent to HTTPS port" return IncorrectRequestError; case 429: return TooManyRequestsError; case 501: case 510: return RequestNotImplementedError; case 511: return NetworkAuthRequiredError; default: return NetworkError; } }(), std::move(msg) }; // clang-format on } QDebug BaseJob::Status::dumpToLog(QDebug dbg) const { QDebugStateSaver _s(dbg); dbg.noquote().nospace(); if (auto* const k = QMetaEnum::fromType().valueToKey(code)) { const QByteArray b = k; dbg << b.mid(b.lastIndexOf(':')); } else dbg << code; return dbg << ": " << message; } BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const { // QNetworkReply error codes seem to be flawed when it comes to HTTP; // see, e.g., https://github.com/quotient-im/libQuotient/issues/200 // so check genuine HTTP codes. The below processing is based on // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes const auto httpCodeHeader = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); if (!httpCodeHeader.isValid()) { qCWarning(d->logCat).noquote() << "No valid HTTP headers from" << d->dumpRequest(); return { NetworkError, reply->errorString() }; } const auto httpCode = httpCodeHeader.toInt(); if (httpCode / 100 == 2) // 2xx { if (reply->isFinished()) qCInfo(d->logCat).noquote() << httpCode << "<-" << d->dumpRequest(); if (!checkContentType(reply->rawHeader("Content-Type"), d->expectedContentTypes)) return { UnexpectedResponseTypeWarning, "Unexpected content type of the response" }; return NoError; } if (reply->isFinished()) qCWarning(d->logCat).noquote() << httpCode << "<-" << d->dumpRequest(); auto message = reply->errorString(); if (message.isEmpty()) message = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute) .toString(); return Status::fromHttpCode(httpCode, message); } BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) { d->rawResponse = reply->readAll(); QJsonParseError error { 0, QJsonParseError::MissingObject }; const auto& json = QJsonDocument::fromJson(d->rawResponse, &error); if (error.error == QJsonParseError::NoError) return parseJson(json); return { IncorrectResponseError, error.errorString() }; } BaseJob::Status BaseJob::parseJson(const QJsonDocument&) { return Success; } BaseJob::Status BaseJob::parseError(QNetworkReply* /*reply*/, const QJsonObject& errorJson) { const auto errCode = errorJson.value("errcode"_ls).toString(); if (error() == TooManyRequestsError || errCode == "M_LIMIT_EXCEEDED") { QString msg = tr("Too many requests"); int64_t retryAfterMs = errorJson.value("retry_after_ms"_ls).toInt(-1); if (retryAfterMs >= 0) msg += tr(", next retry advised after %1 ms").arg(retryAfterMs); else // We still have to figure some reasonable interval retryAfterMs = getNextRetryMs(); d->connection->limitRate(milliseconds(retryAfterMs)); return { TooManyRequestsError, msg }; } if (errCode == "M_CONSENT_NOT_GIVEN") { d->errorUrl = errorJson.value("consent_uri"_ls).toString(); return { UserConsentRequiredError }; } if (errCode == "M_UNSUPPORTED_ROOM_VERSION" || errCode == "M_INCOMPATIBLE_ROOM_VERSION") return { UnsupportedRoomVersionError, errorJson.contains("room_version"_ls) ? tr("Requested room version: %1") .arg(errorJson.value("room_version"_ls).toString()) : errorJson.value("error"_ls).toString() }; if (errCode == "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM") return { CannotLeaveRoom, tr("It's not allowed to leave a server notices room") }; if (errCode == "M_USER_DEACTIVATED") return { UserDeactivated }; // Not localisable on the client side if (errorJson.contains("error"_ls)) d->status.message = errorJson.value("error"_ls).toString(); return d->status; } void BaseJob::stop() { // This method is (also) used to semi-finalise the job before retrying; so // stop the timeout timer but keep the retry timer running. d->timer.stop(); if (d->reply) { d->reply->disconnect(this); // Ignore whatever comes from the reply if (d->reply->isRunning()) { qCWarning(d->logCat) << this << "stopped without ready network reply"; d->reply->abort(); // Keep the reply object in case clients need it } } else qCWarning(d->logCat) << this << "stopped with empty network reply"; } void BaseJob::finishJob() { stop(); if (error() == TooManyRequests) { emit rateLimited(); setStatus(Pending); d->connection->submit(this); return; } if ((error() == NetworkError || error() == Timeout) && d->retriesTaken < d->maxRetries) { // TODO: The whole retrying thing should be put to Connection(Manager) // otherwise independently retrying jobs make a bit of notification // storm towards the UI. const seconds retryIn = error() == Timeout ? 0s : getNextRetryInterval(); ++d->retriesTaken; qCWarning(d->logCat).nospace() << this << ": retry #" << d->retriesTaken << " in " << retryIn.count() << " s"; d->retryTimer.start(retryIn); emit retryScheduled(d->retriesTaken, milliseconds(retryIn).count()); return; } // Notify those interested in any completion of the job including abandon() emit finished(this); emit result(this); // abandon() doesn't emit this if (error()) emit failure(this); else emit success(this); deleteLater(); } seconds BaseJob::getCurrentTimeout() const { return d->getCurrentTimeoutConfig().jobTimeout; } BaseJob::duration_ms_t BaseJob::getCurrentTimeoutMs() const { return milliseconds(getCurrentTimeout()).count(); } seconds BaseJob::getNextRetryInterval() const { return d->getCurrentTimeoutConfig().nextRetryInterval; } BaseJob::duration_ms_t BaseJob::getNextRetryMs() const { return milliseconds(getNextRetryInterval()).count(); } milliseconds BaseJob::timeToRetry() const { return d->retryTimer.isActive() ? d->retryTimer.remainingTimeAsDuration() : 0s; } BaseJob::duration_ms_t BaseJob::millisToRetry() const { return timeToRetry().count(); } int BaseJob::maxRetries() const { return d->maxRetries; } void BaseJob::setMaxRetries(int newMaxRetries) { d->maxRetries = newMaxRetries; } BaseJob::Status BaseJob::status() const { return d->status; } QByteArray BaseJob::rawData(int bytesAtMost) const { return bytesAtMost > 0 && d->rawResponse.size() > bytesAtMost ? d->rawResponse.left(bytesAtMost) : d->rawResponse; } QString BaseJob::rawDataSample(int bytesAtMost) const { auto data = rawData(bytesAtMost); Q_ASSERT(data.size() <= d->rawResponse.size()); return data.size() == d->rawResponse.size() ? data : data + tr("...(truncated, %Ln bytes in total)", "Comes after trimmed raw network response", d->rawResponse.size()); } QString BaseJob::statusCaption() const { switch (d->status.code) { case Success: return tr("Success"); case Pending: return tr("Request still pending response"); case UnexpectedResponseTypeWarning: return tr("Warning: Unexpected response type"); case Abandoned: return tr("Request was abandoned"); case NetworkError: return tr("Network problems"); case TimeoutError: return tr("Request timed out"); case ContentAccessError: return tr("Access error"); case NotFoundError: return tr("Not found"); case IncorrectRequestError: return tr("Invalid request"); case IncorrectResponseError: return tr("Response could not be parsed"); case TooManyRequestsError: return tr("Too many requests"); case RequestNotImplementedError: return tr("Function not implemented by the server"); case NetworkAuthRequiredError: return tr("Network authentication required"); case UserConsentRequiredError: return tr("User consent required"); case UnsupportedRoomVersionError: return tr("The server does not support the needed room version"); default: return tr("Request failed"); } } int BaseJob::error() const { return d->status.code; } QString BaseJob::errorString() const { return d->status.message; } QUrl BaseJob::errorUrl() const { return d->errorUrl; } void BaseJob::setStatus(Status s) { // The crash that led to this code has been reported in // https://github.com/quotient-im/Quaternion/issues/566 - basically, // when cleaning up children of a deleted Connection, there's a chance // of pending jobs being abandoned, calling setStatus(Abandoned). // There's nothing wrong with this; however, the safety check for // cleartext access tokens below uses d->connection - which is a dangling // pointer. // To alleviate that, a stricter condition is applied, that for Abandoned // and to-be-Abandoned jobs the status message will be disregarded entirely. // We could rectify the situation by making d->connection a QPointer<> // (and deriving ConnectionData from QObject, respectively) but it's // a too edge case for the hassle. if (d->status == s) return; if (d->status.code == Abandoned || s.code == Abandoned) s.message.clear(); if (!s.message.isEmpty() && d->connection && !d->connection->accessToken().isEmpty()) s.message.replace(d->connection->accessToken(), "(REDACTED)"); if (!s.good()) qCWarning(d->logCat) << this << "status" << s; d->status = std::move(s); emit statusChanged(d->status); } void BaseJob::setStatus(int code, QString message) { setStatus({ code, std::move(message) }); } void BaseJob::abandon() { beforeAbandon(d->reply ? d->reply.data() : nullptr); setStatus(Abandoned); if (d->reply) d->reply->disconnect(this); emit finished(this); deleteLater(); } void BaseJob::timeout() { setStatus(TimeoutError, "The job has timed out"); finishJob(); } void BaseJob::setLoggingCategory(LoggingCategory lcf) { d->logCat = lcf; } spectral/include/libQuotient/lib/jobs/basejob.h0000644000175000000620000003232713566674122021576 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "../logging.h" #include "requestdata.h" #include #include #include #include class QNetworkReply; class QSslError; namespace Quotient { class ConnectionData; enum class HttpVerb { Get, Put, Post, Delete }; class BaseJob : public QObject { Q_OBJECT Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT) Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries) public: /*! The status code of a job * * Every job is created in Unprepared status; upon calling prepare() * from Connection (if things are fine) it go to Pending status. After * that, the next transition comes after the reply arrives and its contents * are analysed. At any point in time the job can be abandon()ed, causing * it to switch to status Abandoned for a brief period before deletion. */ enum StatusCode { Success = 0, NoError = Success, // To be compatible with Qt conventions Pending = 1, WarningLevel = 20, //< Warnings have codes starting from this UnexpectedResponseType = 21, UnexpectedResponseTypeWarning = UnexpectedResponseType, Unprepared = 25, //< Initial job state is incomplete, hence warning level Abandoned = 50, //< A tiny period between abandoning and object deletion ErrorLevel = 100, //< Errors have codes starting from this NetworkError = 100, Timeout, TimeoutError = Timeout, ContentAccessError, NotFoundError, IncorrectRequest, IncorrectRequestError = IncorrectRequest, IncorrectResponse, IncorrectResponseError = IncorrectResponse, JsonParseError //< \deprecated Use IncorrectResponse instead = IncorrectResponse, TooManyRequests, TooManyRequestsError = TooManyRequests, RateLimited = TooManyRequests, RequestNotImplemented, RequestNotImplementedError = RequestNotImplemented, UnsupportedRoomVersion, UnsupportedRoomVersionError = UnsupportedRoomVersion, NetworkAuthRequired, NetworkAuthRequiredError = NetworkAuthRequired, UserConsentRequired, UserConsentRequiredError = UserConsentRequired, CannotLeaveRoom, UserDeactivated, UserDefinedError = 256 }; Q_ENUM(StatusCode) /** * A simple wrapper around QUrlQuery that allows its creation from * a list of string pairs */ class Query : public QUrlQuery { public: using QUrlQuery::QUrlQuery; Query() = default; Query(const std::initializer_list>& l) { setQueryItems(l); } }; using Data = RequestData; /*! * This structure stores the status of a server call job. The status * consists of a code, that is described (but not delimited) by the * respective enum, and a freeform message. * * To extend the list of error codes, define an (anonymous) enum * along the lines of StatusCode, with additional values * starting at UserDefinedError */ struct Status { Status(StatusCode c) : code(c) {} Status(int c, QString m) : code(c), message(std::move(m)) {} static Status fromHttpCode(int httpCode, QString msg = {}); bool good() const { return code < ErrorLevel; } QDebug dumpToLog(QDebug dbg) const; friend QDebug operator<<(const QDebug& dbg, const Status& s) { return s.dumpToLog(dbg); } bool operator==(const Status& other) const { return code == other.code && message == other.message; } bool operator!=(const Status& other) const { return !operator==(other); } int code; QString message; }; public: BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, bool needsToken = true); BaseJob(HttpVerb verb, const QString& name, const QString& endpoint, const Query& query, Data&& data = {}, bool needsToken = true); QUrl requestUrl() const; bool isBackground() const; /** Current status of the job */ Status status() const; /** Short human-friendly message on the job status */ QString statusCaption() const; /** Get raw response body as received from the server * \param bytesAtMost return this number of leftmost bytes, or -1 * to return the entire response */ QByteArray rawData(int bytesAtMost = -1) const; /** Get UI-friendly sample of raw data * * This is almost the same as rawData but appends the "truncated" * suffix if not all data fit in bytesAtMost. This call is * recommended to present a sample of raw data as "details" next to * error messages. Note that the default \p bytesAtMost value is * also tailored to UI cases. */ QString rawDataSample(int bytesAtMost = 65535) const; /** Error (more generally, status) code * Equivalent to status().code * \sa status */ int error() const; /** Error-specific message, as returned by the server */ virtual QString errorString() const; /** A URL to help/clarify the error, if provided by the server */ QUrl errorUrl() const; int maxRetries() const; void setMaxRetries(int newMaxRetries); using duration_ms_t = std::chrono::milliseconds::rep; // normally int64_t std::chrono::seconds getCurrentTimeout() const; Q_INVOKABLE duration_ms_t getCurrentTimeoutMs() const; std::chrono::seconds getNextRetryInterval() const; Q_INVOKABLE duration_ms_t getNextRetryMs() const; std::chrono::milliseconds timeToRetry() const; Q_INVOKABLE duration_ms_t millisToRetry() const; friend QDebug operator<<(QDebug dbg, const BaseJob* j) { return dbg << j->objectName(); } public slots: void prepare(ConnectionData* connData, bool inBackground); /** * Abandons the result of this job, arrived or unarrived. * * This aborts waiting for a reply from the server (if there was * any pending) and deletes the job object. No result signals * (result, success, failure) are emitted. */ void abandon(); signals: /** The job is about to send a network request */ void aboutToSendRequest(); /** The job has sent a network request */ void sentRequest(); /** The job has changed its status */ void statusChanged(Status newStatus); /** * The previous network request has failed; the next attempt will * be done in the specified time * @param nextAttempt the 1-based number of attempt (will always be more * than 1) * @param inMilliseconds the interval after which the next attempt will be * taken */ void retryScheduled(int nextAttempt, duration_ms_t inMilliseconds); /** * The previous network request has been rate-limited; the next attempt * will be queued and run sometime later. Since other jobs may already * wait in the queue, it's not possible to predict the wait time. */ void rateLimited(); /** * Emitted when the job is finished, in any case. It is used to notify * observers that the job is terminated and that progress can be hidden. * * This should not be emitted directly by subclasses; * use finishJob() instead. * * In general, to be notified of a job's completion, client code * should connect to result(), success(), or failure() * rather than finished(). However if you need to track the job's * lifecycle you should connect to this instead of result(); * in particular, only this signal will be emitted on abandoning. * * @param job the job that emitted this signal * * @see result, success, failure */ void finished(BaseJob* job); /** * Emitted when the job is finished (except when abandoned). * * Use error() to know if the job was finished with error. * * @param job the job that emitted this signal * * @see success, failure */ void result(BaseJob* job); /** * Emitted together with result() in case there's no error. * * @see result, failure */ void success(BaseJob*); /** * Emitted together with result() if there's an error. * Similar to result(), this won't be emitted in case of abandon(). * * @see result, success */ void failure(BaseJob*); void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); void uploadProgress(qint64 bytesSent, qint64 bytesTotal); protected: using headers_t = QHash; const QString& apiEndpoint() const; void setApiEndpoint(const QString& apiEndpoint); const headers_t& requestHeaders() const; void setRequestHeader(const headers_t::key_type& headerName, const headers_t::mapped_type& headerValue); void setRequestHeaders(const headers_t& headers); const QUrlQuery& query() const; void setRequestQuery(const QUrlQuery& query); const Data& requestData() const; void setRequestData(Data&& data); const QByteArrayList& expectedContentTypes() const; void addExpectedContentType(const QByteArray& contentType); void setExpectedContentTypes(const QByteArrayList& contentTypes); /** Construct a URL out of baseUrl, path and query * The function automatically adds '/' between baseUrl's path and * \p path if necessary. The query component of \p baseUrl * is ignored. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& path, const QUrlQuery& query = {}); /*! Prepares the job for execution * * This method is called no more than once per job lifecycle, * when it's first scheduled for execution; in particular, it is not called * on retries. */ virtual void doPrepare(); /*! Postprocessing after the network request has been sent * * This method is called every time the job receives a running * QNetworkReply object from NetworkAccessManager - basically, after * successfully sending a network request (including retries). */ virtual void onSentRequest(QNetworkReply*); virtual void beforeAbandon(QNetworkReply*); /** * Used by gotReply() to check the received reply for general * issues such as network errors or access denial. * Returning anything except NoError/Success prevents * further parseReply()/parseJson() invocation. * * @param reply the reply received from the server * @return the result of checking the reply * * @see gotReply */ virtual Status doCheckReply(QNetworkReply* reply) const; /** * Processes the reply. By default, parses the reply into * a QJsonDocument and calls parseJson() if it's a valid JSON. * * @param reply raw contents of a HTTP reply from the server * * @see gotReply, parseJson */ virtual Status parseReply(QNetworkReply* reply); /** * Processes the JSON document received from the Matrix server. * By default returns successful status without analysing the JSON. * * @param json valid JSON document received from the server * * @see parseReply */ virtual Status parseJson(const QJsonDocument&); /** * Processes the reply in case of unsuccessful HTTP code. * The body is already loaded from the reply object to errorJson. * @param reply the HTTP reply from the server * @param errorJson the JSON payload describing the error */ virtual Status parseError(QNetworkReply*, const QJsonObject& errorJson); void setStatus(Status s); void setStatus(int code, QString message); // Q_DECLARE_LOGGING_CATEGORY return different function types // in different versions using LoggingCategory = decltype(JOBS)*; void setLoggingCategory(LoggingCategory lcf); // Job objects should only be deleted via QObject::deleteLater ~BaseJob() override; protected slots: void timeout(); private slots: void sendRequest(); void checkReply(); void gotReply(); friend class ConnectionData; // to provide access to sendRequest() private: void stop(); void finishJob(); class Private; QScopedPointer d; }; inline bool isJobRunning(BaseJob* job) { return job && job->error() == BaseJob::Pending; } } // namespace Quotient spectral/include/libQuotient/lib/jobs/mediathumbnailjob.h0000644000175000000620000000307213566674122023642 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2016 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "csapi/content-repo.h" #include namespace Quotient { class MediaThumbnailJob : public GetContentThumbnailJob { public: using GetContentThumbnailJob::makeRequestUrl; static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri, QSize requestedSize); MediaThumbnailJob(const QString& serverName, const QString& mediaId, QSize requestedSize); MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize); QImage thumbnail() const; QImage scaledThumbnail(QSize toSize) const; protected: Status parseReply(QNetworkReply* reply) override; private: QImage _thumbnail; }; } // namespace Quotient spectral/include/libQuotient/lib/jobs/syncjob.h0000644000175000000620000000274013566674122021634 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2016 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "../csapi/definitions/sync_filter.h" #include "../syncdata.h" #include "basejob.h" namespace Quotient { class SyncJob : public BaseJob { public: explicit SyncJob(const QString& since = {}, const QString& filter = {}, int timeout = -1, const QString& presence = {}); explicit SyncJob(const QString& since, const Filter& filter, int timeout = -1, const QString& presence = {}); SyncData&& takeData() { return std::move(d); } protected: Status parseJson(const QJsonDocument& data) override; private: SyncData d; }; } // namespace Quotient spectral/include/libQuotient/lib/jobs/downloadfilejob.cpp0000644000175000000620000000756413566674122023673 0ustar dilingerstaff#include "downloadfilejob.h" #include #include #include using namespace Quotient; class DownloadFileJob::Private { public: Private() : tempFile(new QTemporaryFile()) {} explicit Private(const QString& localFilename) : targetFile(new QFile(localFilename)) , tempFile(new QFile(targetFile->fileName() + ".qtntdownload")) {} QScopedPointer targetFile; QScopedPointer tempFile; }; QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri) { return makeRequestUrl(std::move(baseUrl), mxcUri.authority(), mxcUri.path().mid(1)); } DownloadFileJob::DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& localFilename) : GetContentJob(serverName, mediaId) , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) { setObjectName(QStringLiteral("DownloadFileJob")); } QString DownloadFileJob::targetFileName() const { return (d->targetFile ? d->targetFile : d->tempFile)->fileName(); } void DownloadFileJob::doPrepare() { if (d->targetFile && !d->targetFile->isReadable() && !d->targetFile->open(QIODevice::WriteOnly)) { qCWarning(JOBS) << "Couldn't open the file" << d->targetFile->fileName() << "for writing"; setStatus(FileError, "Could not open the target file for writing"); return; } if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly)) { qCWarning(JOBS) << "Couldn't open the temporary file" << d->tempFile->fileName() << "for writing"; setStatus(FileError, "Could not open the temporary download file"); return; } qCDebug(JOBS) << "Downloading to" << d->tempFile->fileName(); } void DownloadFileJob::onSentRequest(QNetworkReply* reply) { connect(reply, &QNetworkReply::metaDataChanged, this, [this, reply] { if (!status().good()) return; auto sizeHeader = reply->header(QNetworkRequest::ContentLengthHeader); if (sizeHeader.isValid()) { auto targetSize = sizeHeader.value(); if (targetSize != -1) if (!d->tempFile->resize(targetSize)) { qCWarning(JOBS) << "Failed to allocate" << targetSize << "bytes for" << d->tempFile->fileName(); setStatus(FileError, "Could not reserve disk space for download"); } } }); connect(reply, &QIODevice::readyRead, this, [this, reply] { if (!status().good()) return; auto bytes = reply->read(reply->bytesAvailable()); if (!bytes.isEmpty()) d->tempFile->write(bytes); else qCWarning(JOBS) << "Unexpected empty chunk when downloading from" << reply->url() << "to" << d->tempFile->fileName(); }); } void DownloadFileJob::beforeAbandon(QNetworkReply*) { if (d->targetFile) d->targetFile->remove(); d->tempFile->remove(); } BaseJob::Status DownloadFileJob::parseReply(QNetworkReply*) { if (d->targetFile) { d->targetFile->close(); if (!d->targetFile->remove()) { qCWarning(JOBS) << "Failed to remove the target file placeholder"; return { FileError, "Couldn't finalise the download" }; } if (!d->tempFile->rename(d->targetFile->fileName())) { qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() << "to" << d->targetFile->fileName(); return { FileError, "Couldn't finalise the download" }; } } else d->tempFile->close(); qCDebug(JOBS) << "Saved a file as" << targetFileName(); return Success; } spectral/include/libQuotient/lib/jobs/downloadfilejob.h0000644000175000000620000000132313566674122023323 0ustar dilingerstaff#pragma once #include "csapi/content-repo.h" namespace Quotient { class DownloadFileJob : public GetContentJob { public: enum { FileError = BaseJob::UserDefinedError + 1 }; using GetContentJob::makeRequestUrl; static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri); DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& localFilename = {}); QString targetFileName() const; private: class Private; QScopedPointer d; void doPrepare() override; void onSentRequest(QNetworkReply* reply) override; void beforeAbandon(QNetworkReply*) override; Status parseReply(QNetworkReply*) override; }; } // namespace Quotient spectral/include/libQuotient/lib/jobs/postreadmarkersjob.h0000644000175000000620000000262313566674122024066 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "basejob.h" #include using namespace Quotient; class PostReadMarkersJob : public BaseJob { public: explicit PostReadMarkersJob(const QString& roomId, const QString& readUpToEventId) : BaseJob( HttpVerb::Post, "PostReadMarkersJob", QStringLiteral("_matrix/client/r0/rooms/%1/read_markers").arg(roomId)) { setRequestData( QJsonObject { { QStringLiteral("m.fully_read"), readUpToEventId } }); } }; spectral/include/libQuotient/lib/jobs/requestdata.h0000644000175000000620000000371713566674122022514 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include class QByteArray; class QJsonObject; class QJsonArray; class QJsonDocument; class QIODevice; namespace Quotient { /** * A simple wrapper that represents the request body. * Provides a unified interface to dump an unstructured byte stream * as well as JSON (and possibly other structures in the future) to * a QByteArray consumed by QNetworkAccessManager request methods. */ class RequestData { public: RequestData() = default; RequestData(const QByteArray& a); RequestData(const QJsonObject& jo); RequestData(const QJsonArray& ja); RequestData(QIODevice* source) : _source(std::unique_ptr(source)) {} RequestData(const RequestData&) = delete; RequestData& operator=(const RequestData&) = delete; RequestData(RequestData&&) = default; RequestData& operator=(RequestData&&) = default; ~RequestData(); QIODevice* source() const { return _source.get(); } private: std::unique_ptr _source; }; } // namespace Quotient /// \deprecated Use namespace Quotient instead namespace QMatrixClient = Quotient; spectral/include/libQuotient/lib/jobs/mediathumbnailjob.cpp0000644000175000000620000000450713566674122024201 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2016 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "mediathumbnailjob.h" using namespace Quotient; QUrl MediaThumbnailJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri, QSize requestedSize) { return makeRequestUrl(std::move(baseUrl), mxcUri.authority(), mxcUri.path().mid(1), requestedSize.width(), requestedSize.height()); } MediaThumbnailJob::MediaThumbnailJob(const QString& serverName, const QString& mediaId, QSize requestedSize) : GetContentThumbnailJob(serverName, mediaId, requestedSize.width(), requestedSize.height()) {} MediaThumbnailJob::MediaThumbnailJob(const QUrl& mxcUri, QSize requestedSize) : MediaThumbnailJob(mxcUri.authority(), mxcUri.path().mid(1), // sans leading '/' requestedSize) {} QImage MediaThumbnailJob::thumbnail() const { return _thumbnail; } QImage MediaThumbnailJob::scaledThumbnail(QSize toSize) const { return _thumbnail.scaled(toSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } BaseJob::Status MediaThumbnailJob::parseReply(QNetworkReply* reply) { auto result = GetContentThumbnailJob::parseReply(reply); if (!result.good()) return result; if (_thumbnail.loadFromData(data()->readAll())) return Success; return { IncorrectResponseError, QStringLiteral("Could not read image data") }; } spectral/include/libQuotient/lib/jobs/syncjob.cpp0000644000175000000620000000442313566674122022167 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2016 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "syncjob.h" using namespace Quotient; static size_t jobId = 0; SyncJob::SyncJob(const QString& since, const QString& filter, int timeout, const QString& presence) : BaseJob(HttpVerb::Get, QStringLiteral("SyncJob-%1").arg(++jobId), QStringLiteral("_matrix/client/r0/sync")) { setLoggingCategory(SYNCJOB); QUrlQuery query; if (!filter.isEmpty()) query.addQueryItem(QStringLiteral("filter"), filter); if (!presence.isEmpty()) query.addQueryItem(QStringLiteral("set_presence"), presence); if (timeout >= 0) query.addQueryItem(QStringLiteral("timeout"), QString::number(timeout)); if (!since.isEmpty()) query.addQueryItem(QStringLiteral("since"), since); setRequestQuery(query); setMaxRetries(std::numeric_limits::max()); } SyncJob::SyncJob(const QString& since, const Filter& filter, int timeout, const QString& presence) : SyncJob(since, QJsonDocument(toJson(filter)).toJson(QJsonDocument::Compact), timeout, presence) {} BaseJob::Status SyncJob::parseJson(const QJsonDocument& data) { d.parseJson(data.object()); if (d.unresolvedRooms().isEmpty()) return BaseJob::Success; qCCritical(MAIN).noquote() << "Incomplete sync response, missing rooms:" << d.unresolvedRooms().join(','); return BaseJob::IncorrectResponseError; } spectral/include/libQuotient/lib/jobs/requestdata.cpp0000644000175000000620000000145413566674122023043 0ustar dilingerstaff#include "requestdata.h" #include #include #include #include #include using namespace Quotient; auto fromData(const QByteArray& data) { auto source = std::make_unique(); source->open(QIODevice::WriteOnly); source->write(data); source->close(); return source; } template inline auto fromJson(const JsonDataT& jdata) { return fromData(QJsonDocument(jdata).toJson(QJsonDocument::Compact)); } RequestData::RequestData(const QByteArray& a) : _source(fromData(a)) {} RequestData::RequestData(const QJsonObject& jo) : _source(fromJson(jo)) {} RequestData::RequestData(const QJsonArray& ja) : _source(fromJson(ja)) {} RequestData::~RequestData() = default; spectral/include/libQuotient/lib/identity/0002755000175000000620000000000013566674122020707 5ustar dilingerstaffspectral/include/libQuotient/lib/identity/definitions/0002755000175000000620000000000013566674122023222 5ustar dilingerstaffspectral/include/libQuotient/lib/identity/definitions/request_email_validation.h0000644000175000000620000000264613566674122030452 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" namespace Quotient { // Data structures struct RequestEmailValidation { /// A unique string generated by the client, and used to identify /// thevalidation attempt. It must be a string consisting of the /// characters``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters /// and itmust not be empty. QString clientSecret; /// The email address to validate. QString email; /// The server will only send an email if the ``send_attempt``is a number /// greater than the most recent one which it has seen,scoped to that /// ``email`` + ``client_secret`` pair. This is toavoid repeatedly sending /// the same email in the case of requestretries between the POSTing user /// and the identity server.The client should increment this value if they /// desire a newemail (e.g. a reminder) to be sent. int sendAttempt; /// Optional. When the validation is completed, the identityserver will /// redirect the user to this URL. QString nextLink; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const RequestEmailValidation& pod); static void fillFrom(const QJsonObject& jo, RequestEmailValidation& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/identity/definitions/request_email_validation.cpp0000644000175000000620000000167313566674122031004 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "request_email_validation.h" using namespace Quotient; void JsonObjectConverter::dumpTo( QJsonObject& jo, const RequestEmailValidation& pod) { addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); addParam<>(jo, QStringLiteral("email"), pod.email); addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); addParam(jo, QStringLiteral("next_link"), pod.nextLink); } void JsonObjectConverter::fillFrom( const QJsonObject& jo, RequestEmailValidation& result) { fromJson(jo.value("client_secret"_ls), result.clientSecret); fromJson(jo.value("email"_ls), result.email); fromJson(jo.value("send_attempt"_ls), result.sendAttempt); fromJson(jo.value("next_link"_ls), result.nextLink); } spectral/include/libQuotient/lib/identity/definitions/sid.cpp0000644000175000000620000000067213566674122024510 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "sid.h" using namespace Quotient; void JsonObjectConverter::dumpTo(QJsonObject& jo, const Sid& pod) { addParam<>(jo, QStringLiteral("sid"), pod.sid); } void JsonObjectConverter::fillFrom(const QJsonObject& jo, Sid& result) { fromJson(jo.value("sid"_ls), result.sid); } spectral/include/libQuotient/lib/identity/definitions/sid.h0000644000175000000620000000127013566674122024150 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" namespace Quotient { // Data structures struct Sid { /// The session ID. Session IDs are opaque strings generated by the /// identityserver. They must consist entirely of the /// characters``[0-9a-zA-Z.=_-]``. Their length must not exceed 255 /// characters and theymust not be empty. QString sid; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const Sid& pod); static void fillFrom(const QJsonObject& jo, Sid& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/identity/definitions/request_msisdn_validation.cpp0000644000175000000620000000211413566674122031201 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #include "request_msisdn_validation.h" using namespace Quotient; void JsonObjectConverter::dumpTo( QJsonObject& jo, const RequestMsisdnValidation& pod) { addParam<>(jo, QStringLiteral("client_secret"), pod.clientSecret); addParam<>(jo, QStringLiteral("country"), pod.country); addParam<>(jo, QStringLiteral("phone_number"), pod.phoneNumber); addParam<>(jo, QStringLiteral("send_attempt"), pod.sendAttempt); addParam(jo, QStringLiteral("next_link"), pod.nextLink); } void JsonObjectConverter::fillFrom( const QJsonObject& jo, RequestMsisdnValidation& result) { fromJson(jo.value("client_secret"_ls), result.clientSecret); fromJson(jo.value("country"_ls), result.country); fromJson(jo.value("phone_number"_ls), result.phoneNumber); fromJson(jo.value("send_attempt"_ls), result.sendAttempt); fromJson(jo.value("next_link"_ls), result.nextLink); } spectral/include/libQuotient/lib/identity/definitions/request_msisdn_validation.h0000644000175000000620000000314713566674122030655 0ustar dilingerstaff/****************************************************************************** * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN */ #pragma once #include "converters.h" namespace Quotient { // Data structures struct RequestMsisdnValidation { /// A unique string generated by the client, and used to identify /// thevalidation attempt. It must be a string consisting of the /// characters``[0-9a-zA-Z.=_-]``. Its length must not exceed 255 characters /// and itmust not be empty. QString clientSecret; /// The two-letter uppercase ISO country code that the number /// in``phone_number`` should be parsed as if it were dialled from. QString country; /// The phone number to validate. QString phoneNumber; /// The server will only send an SMS if the ``send_attempt`` is anumber /// greater than the most recent one which it has seen,scoped to that /// ``country`` + ``phone_number`` + ``client_secret``triple. This is to /// avoid repeatedly sending the same SMS inthe case of request retries /// between the POSTing user and theidentity server. The client should /// increment this value ifthey desire a new SMS (e.g. a reminder) to be /// sent. int sendAttempt; /// Optional. When the validation is completed, the identityserver will /// redirect the user to this URL. QString nextLink; }; template <> struct JsonObjectConverter { static void dumpTo(QJsonObject& jo, const RequestMsisdnValidation& pod); static void fillFrom(const QJsonObject& jo, RequestMsisdnValidation& pod); }; } // namespace Quotient spectral/include/libQuotient/lib/room.h0000644000175000000620000006675013566674122020217 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include "connection.h" #include "eventitem.h" #include "joinstate.h" #include "csapi/message_pagination.h" #include "events/accountdataevents.h" #include "events/encryptedevent.h" #include "events/roommessageevent.h" #include #include #include #include #include namespace Quotient { class Event; class Avatar; class SyncRoomData; class RoomMemberEvent; class User; class MemberSorter; class LeaveRoomJob; class SetRoomStateWithKeyJob; class RedactEventJob; /** The data structure used to expose file transfer information to views * * This is specifically tuned to work with QML exposing all traits as * Q_PROPERTY values. */ class FileTransferInfo { Q_GADGET Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT) Q_PROPERTY(bool active READ active CONSTANT) Q_PROPERTY(bool started READ started CONSTANT) Q_PROPERTY(bool completed READ completed CONSTANT) Q_PROPERTY(bool failed READ failed CONSTANT) Q_PROPERTY(int progress MEMBER progress CONSTANT) Q_PROPERTY(int total MEMBER total CONSTANT) Q_PROPERTY(QUrl localDir MEMBER localDir CONSTANT) Q_PROPERTY(QUrl localPath MEMBER localPath CONSTANT) public: enum Status { None, Started, Completed, Failed, Cancelled }; Status status = None; bool isUpload = false; int progress = 0; int total = -1; QUrl localDir {}; QUrl localPath {}; bool started() const { return status == Started; } bool completed() const { return status == Completed; } bool active() const { return started() || completed(); } bool failed() const { return status == Failed; } }; class Room : public QObject { Q_OBJECT Q_PROPERTY(Connection* connection READ connection CONSTANT) Q_PROPERTY(User* localUser READ localUser CONSTANT) Q_PROPERTY(QString id READ id CONSTANT) Q_PROPERTY(QString version READ version NOTIFY baseStateLoaded) Q_PROPERTY(bool isUnstable READ isUnstable NOTIFY stabilityUpdated) Q_PROPERTY(QString predecessorId READ predecessorId NOTIFY baseStateLoaded) Q_PROPERTY(QString successorId READ successorId NOTIFY upgraded) Q_PROPERTY(QString name READ name NOTIFY namesChanged) Q_PROPERTY(QStringList localAliases READ localAliases NOTIFY namesChanged) Q_PROPERTY(QStringList remoteAliases READ remoteAliases NOTIFY namesChanged) Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged STORED false) Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) Q_PROPERTY(bool usesEncryption READ usesEncryption NOTIFY encryption) Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged) Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged) Q_PROPERTY(int joinedCount READ joinedCount NOTIFY memberListChanged) Q_PROPERTY(int invitedCount READ invitedCount NOTIFY memberListChanged) Q_PROPERTY(int totalMemberCount READ totalMemberCount NOTIFY memberListChanged) Q_PROPERTY(bool displayed READ displayed WRITE setDisplayed NOTIFY displayedChanged) Q_PROPERTY(QString firstDisplayedEventId READ firstDisplayedEventId WRITE setFirstDisplayedEventId NOTIFY firstDisplayedEventChanged) Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE setLastDisplayedEventId NOTIFY lastDisplayedEventChanged) Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved) Q_PROPERTY(bool hasUnreadMessages READ hasUnreadMessages NOTIFY unreadMessagesChanged) Q_PROPERTY(int unreadCount READ unreadCount NOTIFY unreadMessagesChanged) Q_PROPERTY(int highlightCount READ highlightCount NOTIFY highlightCountChanged RESET resetHighlightCount) Q_PROPERTY(int notificationCount READ notificationCount NOTIFY notificationCountChanged RESET resetNotificationCount) Q_PROPERTY(bool allHistoryLoaded READ allHistoryLoaded NOTIFY addedMessages STORED false) Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged) Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged) Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged) Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY eventsHistoryJobChanged) public: using Timeline = std::deque; using PendingEvents = std::vector; using RelatedEvents = QVector; using rev_iter_t = Timeline::const_reverse_iterator; using timeline_iter_t = Timeline::const_iterator; enum Change : uint { NoChange = 0x0, NameChange = 0x1, CanonicalAliasChange = 0x2, TopicChange = 0x4, UnreadNotifsChange = 0x8, AvatarChange = 0x10, JoinStateChange = 0x20, TagsChange = 0x40, MembersChange = 0x80, /* = 0x100, */ AccountDataChange = 0x200, SummaryChange = 0x400, ReadMarkerChange = 0x800, OtherChange = 0x8000, AnyChange = 0xFFFF }; Q_DECLARE_FLAGS(Changes, Change) Q_FLAG(Changes) Room(Connection* connection, QString id, JoinState initialJoinState); ~Room() override; // Property accessors Connection* connection() const; User* localUser() const; const QString& id() const; QString version() const; bool isUnstable() const; QString predecessorId() const; QString successorId() const; QString name() const; /// Room aliases defined on the current user's server /// \sa remoteAliases, setLocalAliases QStringList localAliases() const; /// Room aliases defined on other servers /// \sa localAliases QStringList remoteAliases() const; QString canonicalAlias() const; QString displayName() const; QString topic() const; QString avatarMediaId() const; QUrl avatarUrl() const; const Avatar& avatarObject() const; Q_INVOKABLE JoinState joinState() const; Q_INVOKABLE QList usersTyping() const; QList membersLeft() const; Q_INVOKABLE QList users() const; QStringList memberNames() const; [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]] int memberCount() const; int timelineSize() const; bool usesEncryption() const; RoomEventPtr decryptMessage(EncryptedEvent* encryptedEvent); QString decryptMessage(QJsonObject personalCipherObject, QByteArray senderKey); QString sessionKey(const QString& senderKey, const QString& deviceId, const QString& sessionId) const; QString decryptMessage(QByteArray cipher, const QString& senderKey, const QString& deviceId, const QString& sessionId); int joinedCount() const; int invitedCount() const; int totalMemberCount() const; GetRoomEventsJob* eventsHistoryJob() const; /** * Returns a square room avatar with the given size and requests it * from the network if needed * \return a pixmap with the avatar or a placeholder if there's none * available yet */ Q_INVOKABLE QImage avatar(int dimension); /** * Returns a room avatar with the given dimensions and requests it * from the network if needed * \return a pixmap with the avatar or a placeholder if there's none * available yet */ Q_INVOKABLE QImage avatar(int width, int height); /** * \brief Get a user object for a given user id * This is the recommended way to get a user object in a room * context. The actual object type may be changed in further * versions to provide room-specific user information (display name, * avatar etc.). * \note The method will return a valid user regardless of * the membership. */ Q_INVOKABLE User* user(const QString& userId) const; /** * \brief Check the join state of a given user in this room * * \note Banned and invited users are not tracked for now (Leave * will be returned for them). * * \return either of Join, Leave, depending on the given * user's state in the room */ Q_INVOKABLE JoinState memberJoinState(User* user) const; /** * Get a disambiguated name for a given user in * the context of the room */ Q_INVOKABLE QString roomMembername(const User* u) const; /** * Get a disambiguated name for a user with this id in * the context of the room */ Q_INVOKABLE QString roomMembername(const QString& userId) const; const Timeline& messageEvents() const; const PendingEvents& pendingEvents() const; /// Check whether all historical messages are already loaded /** * \return true if the "oldest" event in the timeline is * a room creation event and there's no further history * to load; false otherwise */ bool allHistoryLoaded() const; /** * A convenience method returning the read marker to the position * before the "oldest" event; same as messageEvents().crend() */ rev_iter_t historyEdge() const; /** * A convenience method returning the iterator beyond the latest * arrived event; same as messageEvents().cend() */ Timeline::const_iterator syncEdge() const; /// \deprecated Use historyEdge instead rev_iter_t timelineEdge() const; Q_INVOKABLE TimelineItem::index_t minTimelineIndex() const; Q_INVOKABLE TimelineItem::index_t maxTimelineIndex() const; Q_INVOKABLE bool isValidIndex(TimelineItem::index_t timelineIndex) const; rev_iter_t findInTimeline(TimelineItem::index_t index) const; rev_iter_t findInTimeline(const QString& evtId) const; PendingEvents::iterator findPendingEvent(const QString& txnId); PendingEvents::const_iterator findPendingEvent(const QString& txnId) const; const RelatedEvents relatedEvents(const QString& evtId, const char* relType) const; const RelatedEvents relatedEvents(const RoomEvent& evt, const char* relType) const; bool displayed() const; /// Mark the room as currently displayed to the user /** * Marking the room displayed causes the room to obtain the full * list of members if it's been lazy-loaded before; in the future * it may do more things bound to "screen time" of the room, e.g. * measure that "screen time". */ void setDisplayed(bool displayed = true); QString firstDisplayedEventId() const; rev_iter_t firstDisplayedMarker() const; void setFirstDisplayedEventId(const QString& eventId); void setFirstDisplayedEvent(TimelineItem::index_t index); QString lastDisplayedEventId() const; rev_iter_t lastDisplayedMarker() const; void setLastDisplayedEventId(const QString& eventId); void setLastDisplayedEvent(TimelineItem::index_t index); rev_iter_t readMarker(const User* user) const; rev_iter_t readMarker() const; QString readMarkerEventId() const; QList usersAtEventId(const QString& eventId); /** * \brief Mark the event with uptoEventId as read * * Finds in the timeline and marks as read the event with * the specified id; also posts a read receipt to the server either * for this message or, if it's from the local user, for * the nearest non-local message before. uptoEventId must be non-empty. */ void markMessagesAsRead(QString uptoEventId); /// Check whether there are unread messages in the room bool hasUnreadMessages() const; /** Get the number of unread messages in the room * Depending on the read marker state, this call may return either * a precise or an estimate number of unread events. Only "notable" * events (non-redacted message events from users other than local) * are counted. * * In a case when readMarker() == timelineEdge() (the local read * marker is beyond the local timeline) only the bottom limit of * the unread messages number can be estimated (and even that may * be slightly off due to, e.g., redactions of events not loaded * to the local timeline). * * If all messages are read, this function will return -1 (_not_ 0, * as zero may mean "zero or more unread messages" in a situation * when the read marker is outside the local timeline. */ int unreadCount() const; Q_INVOKABLE int notificationCount() const; Q_INVOKABLE void resetNotificationCount(); Q_INVOKABLE int highlightCount() const; Q_INVOKABLE void resetHighlightCount(); /** Check whether the room has account data of the given type * Tags and read markers are not supported by this method _yet_. */ bool hasAccountData(const QString& type) const; /** Get a generic account data event of the given type * This returns a generic hash map for any room account data event * stored on the server. Tags and read markers cannot be retrieved * using this method _yet_. */ const EventPtr& accountData(const QString& type) const; QStringList tagNames() const; TagsMap tags() const; TagRecord tag(const QString& name) const; /** Add a new tag to this room * If this room already has this tag, nothing happens. If it's a new * tag for the room, the respective tag record is added to the set * of tags and the new set is sent to the server to update other * clients. */ void addTag(const QString& name, const TagRecord& record = {}); Q_INVOKABLE void addTag(const QString& name, float order); /// Remove a tag from the room Q_INVOKABLE void removeTag(const QString& name); /** Overwrite the room's tags * This completely replaces the existing room's tags with a set * of new ones and updates the new set on the server. Unlike * most other methods in Room, this one sends a signal about changes * immediately, not waiting for confirmation from the server * (because tags are saved in account data rather than in shared * room state). */ void setTags(TagsMap newTags); /// Check whether the list of tags has m.favourite bool isFavourite() const; /// Check whether the list of tags has m.lowpriority bool isLowPriority() const; /// Check whether this room is for server notices (MSC1452) bool isServerNoticeRoom() const; /// Check whether this room is a direct chat Q_INVOKABLE bool isDirectChat() const; /// Get the list of users this room is a direct chat with QList directChatUsers() const; Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId) const; Q_INVOKABLE QUrl urlToDownload(const QString& eventId) const; /// Get a file name for downloading for a given event id /*! * The event MUST be RoomMessageEvent and have content * for downloading. \sa RoomMessageEvent::hasContent */ Q_INVOKABLE QString fileNameToDownload(const QString& eventId) const; /// Get information on file upload/download /*! * \param id uploads are identified by the corresponding event's * transactionId (because uploads are done before * the event is even sent), while downloads are using * the normal event id for identifier. */ Q_INVOKABLE FileTransferInfo fileTransferInfo(const QString& id) const; /// Get the URL to the actual file source in a unified way /*! * For uploads it will return a URL to a local file; for downloads * the URL will be taken from the corresponding room event. */ Q_INVOKABLE QUrl fileSource(const QString& id) const; /** Pretty-prints plain text into HTML * As of now, it's exactly the same as Quotient::prettyPrint(); * in the future, it will also linkify room aliases, mxids etc. * using the room context. */ Q_INVOKABLE QString prettyPrint(const QString& plainText) const; MemberSorter memberSorter() const; Q_INVOKABLE bool supportsCalls() const; /// Get a state event with the given event type and state key /*! This method returns a (potentially empty) state event corresponding * to the pair of event type \p evtType and state key \p stateKey. */ Q_INVOKABLE const StateEventBase* getCurrentState(const QString& evtType, const QString& stateKey = {}) const; template const EvT* getCurrentState(const QString& stateKey = {}) const { const auto* evt = eventCast(getCurrentState(EvT::matrixTypeId(), stateKey)); Q_ASSERT(evt); Q_ASSERT(evt->matrixTypeId() == EvT::matrixTypeId() && evt->stateKey() == stateKey); return evt; } template auto setState(ArgTs&&... args) const { return setState(EvT(std::forward(args)...)); } public slots: /** Check whether the room should be upgraded */ void checkVersion(); QString postMessage(const QString& plainText, MessageEventType type); QString postPlainText(const QString& plainText); QString postHtmlMessage(const QString& plainText, const QString& html, MessageEventType type = MessageEventType::Text); QString postHtmlText(const QString& plainText, const QString& html); /// Send a reaction on a given event with a given key QString postReaction(const QString& eventId, const QString& key); QString postFile(const QString& plainText, const QUrl& localPath, bool asGenericFile = false); /** Post a pre-created room message event * * Takes ownership of the event, deleting it once the matching one * arrives with the sync * \return transaction id associated with the event. */ QString postEvent(RoomEvent* event); QString postJson(const QString& matrixType, const QJsonObject& eventContent); QString retryMessage(const QString& txnId); void discardMessage(const QString& txnId); /// Send a request to update the room state with the given event SetRoomStateWithKeyJob* setState(const StateEventBase& evt) const; void setName(const QString& newName); void setCanonicalAlias(const QString& newAlias); /// Set room aliases on the user's current server void setLocalAliases(const QStringList& aliases); void setTopic(const QString& newTopic); /// You shouldn't normally call this method; it's here for debugging void refreshDisplayName(); void getPreviousContent(int limit = 10); void inviteToRoom(const QString& memberId); LeaveRoomJob* leaveRoom(); /// \deprecated - use setState() instead") SetRoomStateWithKeyJob* setMemberState(const QString& memberId, const RoomMemberEvent& event) const; void kickMember(const QString& memberId, const QString& reason = {}); void ban(const QString& userId, const QString& reason = {}); void unban(const QString& userId); void redactEvent(const QString& eventId, const QString& reason = {}); void uploadFile(const QString& id, const QUrl& localFilename, const QString& overrideContentType = {}); // If localFilename is empty a temporary file is created void downloadFile(const QString& eventId, const QUrl& localFilename = {}); void cancelFileTransfer(const QString& id); /// Mark all messages in the room as read void markAllMessagesAsRead(); /// Whether the current user is allowed to upgrade the room bool canSwitchVersions() const; /// Switch the room's version (aka upgrade) void switchVersion(QString newVersion); void inviteCall(const QString& callId, const int lifetime, const QString& sdp); void sendCallCandidates(const QString& callId, const QJsonArray& candidates); void answerCall(const QString& callId, const int lifetime, const QString& sdp); void answerCall(const QString& callId, const QString& sdp); void hangupCall(const QString& callId); signals: /// Initial set of state events has been loaded /** * The initial set is what comes from the initial sync for the room. * This includes all basic things like RoomCreateEvent, * RoomNameEvent, a (lazy-loaded, not full) set of RoomMemberEvents * etc. This is a per-room reflection of Connection::loadedRoomState * \sa Connection::loadedRoomState */ void baseStateLoaded(); void eventsHistoryJobChanged(); void aboutToAddHistoricalMessages(RoomEventsRange events); void aboutToAddNewMessages(RoomEventsRange events); void addedMessages(int fromIndex, int toIndex); /// The event is about to be appended to the list of pending events void pendingEventAboutToAdd(RoomEvent* event); /// An event has been appended to the list of pending events void pendingEventAdded(); /// The remote echo has arrived with the sync and will be merged /// with its local counterpart /** NB: Requires a sync loop to be emitted */ void pendingEventAboutToMerge(RoomEvent* serverEvent, int pendingEventIndex); /// The remote and local copies of the event have been merged /** NB: Requires a sync loop to be emitted */ void pendingEventMerged(); /// An event will be removed from the list of pending events void pendingEventAboutToDiscard(int pendingEventIndex); /// An event has just been removed from the list of pending events void pendingEventDiscarded(); /// The status of a pending event has changed /** \sa PendingEventItem::deliveryStatus */ void pendingEventChanged(int pendingEventIndex); /// The server accepted the message /** This is emitted when an event sending request has successfully * completed. This does not mean that the event is already in the * local timeline, only that the server has accepted it. * \param txnId transaction id assigned by the client during sending * \param eventId event id assigned by the server upon acceptance * \sa postEvent, postPlainText, postMessage, postHtmlMessage * \sa pendingEventMerged, aboutToAddNewMessages */ void messageSent(QString txnId, QString eventId); /** A common signal for various kinds of changes in the room * Aside from all changes in the room state * @param changes a set of flags describing what changes occurred * upon the last sync * \sa Changes */ void changed(Changes changes); /** * \brief The room name, the canonical alias or other aliases changed * * Not triggered when display name changes. */ void namesChanged(Room* room); void displaynameAboutToChange(Room* room); void displaynameChanged(Room* room, QString oldName); void topicChanged(); void avatarChanged(); void userAdded(User* user); void userRemoved(User* user); void memberAboutToRename(User* user, QString newName); void memberRenamed(User* user); /// The list of members has changed /** Emitted no more than once per sync, this is a good signal to * for cases when some action should be done upon any change in * the member list. If you need per-item granularity you should use * userAdded, userRemoved and memberAboutToRename / memberRenamed * instead. */ void memberListChanged(); /// The previously lazy-loaded members list is now loaded entirely /// \sa setDisplayed void allMembersLoaded(); void encryption(); void joinStateChanged(JoinState oldState, JoinState newState); void typingChanged(); void highlightCountChanged(); void notificationCountChanged(); void displayedChanged(bool displayed); void firstDisplayedEventChanged(); void lastDisplayedEventChanged(); void lastReadEventChanged(User* user); void readMarkerMoved(QString fromEventId, QString toEventId); void readMarkerForUserMoved(User* user, QString fromEventId, QString toEventId); void unreadMessagesChanged(Room* room); void accountDataAboutToChange(QString type); void accountDataChanged(QString type); void tagsAboutToChange(); void tagsChanged(); void updatedEvent(QString eventId); void replacedEvent(const RoomEvent* newEvent, const RoomEvent* oldEvent); void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl); void fileTransferFailed(QString id, QString errorMessage = {}); void fileTransferCancelled(QString id); void callEvent(Room* room, const RoomEvent* event); /// The room's version stability may have changed void stabilityUpdated(QString recommendedDefault, QStringList stableVersions); /// This room has been upgraded and won't receive updates any more void upgraded(QString serverMessage, Room* successor); /// An attempted room upgrade has failed void upgradeFailed(QString errorMessage); /// The room is about to be deleted void beforeDestruction(Room*); protected: virtual Changes processStateEvent(const RoomEvent& e); virtual Changes processEphemeralEvent(EventPtr&& event); virtual Changes processAccountDataEvent(EventPtr&& event); virtual void onAddNewTimelineEvents(timeline_iter_t /*from*/) {} virtual void onAddHistoricalTimelineEvents(rev_iter_t /*from*/) {} virtual void onRedaction(const RoomEvent& /*prevEvent*/, const RoomEvent& /*after*/) {} virtual QJsonObject toJson() const; virtual void updateData(SyncRoomData&& data, bool fromCache = false); private: friend class Connection; class Private; Private* d; // This is called from Connection, reflecting a state change that // arrived from the server. Clients should use // Connection::joinRoom() and Room::leaveRoom() to change the state. void setJoinState(JoinState state); }; class MemberSorter { public: explicit MemberSorter(const Room* r) : room(r) {} bool operator()(User* u1, User* u2) const; bool operator()(User* u1, const QString& u2name) const; template typename ContT::size_type lowerBoundIndex(const ContT& c, const ValT& v) const { return std::lower_bound(c.begin(), c.end(), v, *this) - c.begin(); } private: const Room* room; }; } // namespace Quotient Q_DECLARE_METATYPE(Quotient::FileTransferInfo) Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::Room::Changes) spectral/include/libQuotient/lib/connectiondata.h0000644000175000000620000000357713566674122022232 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include #include #include class QNetworkAccessManager; namespace Quotient { class BaseJob; class ConnectionData { public: explicit ConnectionData(QUrl baseUrl); virtual ~ConnectionData(); void submit(BaseJob* job); void limitRate(std::chrono::milliseconds nextCallAfter); QByteArray accessToken() const; QUrl baseUrl() const; const QString& deviceId() const; const QString& userId() const; QNetworkAccessManager* nam() const; void setBaseUrl(QUrl baseUrl); void setToken(QByteArray accessToken); [[deprecated("Use setBaseUrl() instead")]] void setHost(QString host); [[deprecated("Use setBaseUrl() instead")]] void setPort(int port); void setDeviceId(const QString& deviceId); void setUserId(const QString& userId); QString lastEvent() const; void setLastEvent(QString identifier); QByteArray generateTxnId() const; private: class Private; std::unique_ptr d; }; } // namespace Quotient spectral/include/libQuotient/lib/logging.cpp0000644000175000000620000000272713566674122021216 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2017 Elvis Angelaccio * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "logging.h" #define LOGGING_CATEGORY(Name, Id) Q_LOGGING_CATEGORY((Name), (Id), QtInfoMsg) // Use LOGGING_CATEGORY instead of Q_LOGGING_CATEGORY in the rest of the code LOGGING_CATEGORY(MAIN, "quotient.main") LOGGING_CATEGORY(EVENTS, "quotient.events") LOGGING_CATEGORY(STATE, "quotient.events.state") LOGGING_CATEGORY(MESSAGES, "quotient.events.messages") LOGGING_CATEGORY(EPHEMERAL, "quotient.events.ephemeral") LOGGING_CATEGORY(E2EE, "quotient.e2ee") LOGGING_CATEGORY(JOBS, "quotient.jobs") LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync") LOGGING_CATEGORY(PROFILER, "quotient.profiler") spectral/include/libQuotient/lib/networkaccessmanager.h0000644000175000000620000000313613566674122023436 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2018 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include #include namespace Quotient { class NetworkAccessManager : public QNetworkAccessManager { Q_OBJECT public: NetworkAccessManager(QObject* parent = nullptr); ~NetworkAccessManager() override; QList ignoredSslErrors() const; void addIgnoredSslError(const QSslError& error); void clearIgnoredSslErrors(); /** Get a pointer to the singleton */ static NetworkAccessManager* instance(); private: QNetworkReply* createRequest(Operation op, const QNetworkRequest& request, QIODevice* outgoingData = Q_NULLPTR) override; class Private; std::unique_ptr d; }; } // namespace Quotient spectral/include/libQuotient/lib/settings.cpp0000644000175000000620000001152613566674122021425 0ustar dilingerstaff#include "settings.h" #include "logging.h" #include using namespace Quotient; QString Settings::legacyOrganizationName {}; QString Settings::legacyApplicationName {}; void Settings::setLegacyNames(const QString& organizationName, const QString& applicationName) { legacyOrganizationName = organizationName; legacyApplicationName = applicationName; } void Settings::setValue(const QString& key, const QVariant& value) { QSettings::setValue(key, value); if (legacySettings.contains(key)) legacySettings.remove(key); } void Settings::remove(const QString& key) { QSettings::remove(key); if (legacySettings.contains(key)) legacySettings.remove(key); } QVariant Settings::value(const QString& key, const QVariant& defaultValue) const { auto value = QSettings::value(key, legacySettings.value(key, defaultValue)); // QML's Qt.labs.Settings stores boolean values as strings, which, if loaded // through the usual QSettings interface, confuses QML // (QVariant("false") == true in JavaScript). Since we have a mixed // environment where both QSettings and Qt.labs.Settings may potentially // work with same settings, better ensure compatibility. return value.toString() == QStringLiteral("false") ? QVariant(false) : value; } bool Settings::contains(const QString& key) const { return QSettings::contains(key) || legacySettings.contains(key); } QStringList Settings::childGroups() const { auto l = QSettings::childGroups(); for (const auto& g: legacySettings.childGroups()) if (!l.contains(g)) l.push_back(g); return l; } void SettingsGroup::setValue(const QString& key, const QVariant& value) { Settings::setValue(groupPath + '/' + key, value); } bool SettingsGroup::contains(const QString& key) const { return Settings::contains(groupPath + '/' + key); } QVariant SettingsGroup::value(const QString& key, const QVariant& defaultValue) const { return Settings::value(groupPath + '/' + key, defaultValue); } QString SettingsGroup::group() const { return groupPath; } QStringList SettingsGroup::childGroups() const { const_cast(this)->beginGroup(groupPath); const_cast(legacySettings).beginGroup(groupPath); QStringList l = Settings::childGroups(); const_cast(this)->endGroup(); const_cast(legacySettings).endGroup(); return l; } void SettingsGroup::remove(const QString& key) { QString fullKey { groupPath }; if (!key.isEmpty()) fullKey += "/" + key; Settings::remove(fullKey); } QTNT_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, setDeviceId) QTNT_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, setDeviceName) QTNT_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn) static const auto HomeserverKey = QStringLiteral("homeserver"); static const auto AccessTokenKey = QStringLiteral("access_token"); static const auto EncryptionAccountPickleKey = QStringLiteral("encryption_account_pickle"); QUrl AccountSettings::homeserver() const { return QUrl::fromUserInput(value(HomeserverKey).toString()); } void AccountSettings::setHomeserver(const QUrl& url) { setValue(HomeserverKey, url.toString()); } QString AccountSettings::userId() const { return group().section('/', -1); } QString AccountSettings::accessToken() const { return value(AccessTokenKey).toString(); } void AccountSettings::setAccessToken(const QString& accessToken) { qCWarning(MAIN) << "Saving access_token to QSettings is insecure." " Developers, do it manually or contribute to share " "QtKeychain logic to libQuotient."; setValue(AccessTokenKey, accessToken); } void AccountSettings::clearAccessToken() { legacySettings.remove(AccessTokenKey); legacySettings.remove(QStringLiteral("device_id")); // Force the server to // re-issue it remove(AccessTokenKey); } QByteArray AccountSettings::encryptionAccountPickle() { QString passphrase = ""; // FIXME: add QtKeychain return value("encryption_account_pickle", "").toByteArray(); } void AccountSettings::setEncryptionAccountPickle( const QByteArray& encryptionAccountPickle) { qCWarning(MAIN) << "Saving encryption_account_pickle to QSettings is insecure." " Developers, do it manually or contribute to share QtKeychain " "logic to libQuotient."; QString passphrase = ""; // FIXME: add QtKeychain setValue("encryption_account_pickle", encryptionAccountPickle); } void AccountSettings::clearEncryptionAccountPickle() { remove(EncryptionAccountPickleKey); // TODO: Force to re-issue it? } spectral/include/libQuotient/lib/settings.h0000644000175000000620000001534313566674122021073 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2016 Kitsune Ral * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #pragma once #include #include #include class QVariant; namespace Quotient { class Settings : public QSettings { Q_OBJECT public: /// Add a legacy organisation/application name to migrate settings from /*! * Use this function before creating any Settings objects in order * to set a legacy location where configuration has previously been stored. * This will provide an additional fallback in case of renaming * the organisation/application. Values in legacy locations are _removed_ * when setValue() or remove() is called. */ static void setLegacyNames(const QString& organizationName, const QString& applicationName = {}); using QSettings::QSettings; /// Set the value for a given key /*! If the key exists in the legacy location, it is removed. */ Q_INVOKABLE void setValue(const QString& key, const QVariant& value); /// Remove the value from both the primary and legacy locations Q_INVOKABLE void remove(const QString& key); /// Obtain a value for a given key /*! * If the key doesn't exist in the primary settings location, the legacy * location is checked. If neither location has the key, * \p defaultValue is returned. * * This function returns a QVariant; use get<>() to get the unwrapped * value if you know the type upfront. * * \sa setLegacyNames, get */ Q_INVOKABLE QVariant value(const QString& key, const QVariant& defaultValue = {}) const; /// Obtain a value for a given key, coerced to the given type /*! * On top of value(), this function unwraps the QVariant and returns * its contents assuming the type passed as the template parameter. * If the type is different from the one stored inside the QVariant, * \p defaultValue is returned. In presence of legacy settings, * only the first found value is checked; if its type does not match, * further checks through legacy settings are not performed and * \p defaultValue is returned. */ template T get(const QString& key, const T& defaultValue = {}) const { const auto qv = value(key, QVariant()); return qv.isValid() && qv.canConvert() ? qv.value() : defaultValue; } Q_INVOKABLE bool contains(const QString& key) const; Q_INVOKABLE QStringList childGroups() const; private: static QString legacyOrganizationName; static QString legacyApplicationName; protected: QSettings legacySettings { legacyOrganizationName, legacyApplicationName }; }; class SettingsGroup : public Settings { public: template explicit SettingsGroup(QString path, ArgTs&&... qsettingsArgs) : Settings(std::forward(qsettingsArgs)...) , groupPath(std::move(path)) {} Q_INVOKABLE bool contains(const QString& key) const; Q_INVOKABLE QVariant value(const QString& key, const QVariant& defaultValue = {}) const; template T get(const QString& key, const T& defaultValue = {}) const { const auto qv = value(key, QVariant()); return qv.isValid() && qv.canConvert() ? qv.value() : defaultValue; } Q_INVOKABLE QString group() const; Q_INVOKABLE QStringList childGroups() const; Q_INVOKABLE void setValue(const QString& key, const QVariant& value); Q_INVOKABLE void remove(const QString& key); private: QString groupPath; }; #define QTNT_DECLARE_SETTING(type, propname, setter) \ Q_PROPERTY(type propname READ propname WRITE setter) \ public: \ type propname() const; \ void setter(type newValue); \ \ private: #define QTNT_DEFINE_SETTING(classname, type, propname, qsettingname, \ defaultValue, setter) \ type classname::propname() const \ { \ return get(QStringLiteral(qsettingname), defaultValue); \ } \ \ void classname::setter(type newValue) \ { \ setValue(QStringLiteral(qsettingname), std::move(newValue)); \ } class AccountSettings : public SettingsGroup { Q_OBJECT Q_PROPERTY(QString userId READ userId CONSTANT) QTNT_DECLARE_SETTING(QString, deviceId, setDeviceId) QTNT_DECLARE_SETTING(QString, deviceName, setDeviceName) QTNT_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) /** \deprecated \sa setAccessToken */ Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken) Q_PROPERTY(QByteArray encryptionAccountPickle READ encryptionAccountPickle WRITE setEncryptionAccountPickle) public: template explicit AccountSettings(const QString& accountId, ArgTs... qsettingsArgs) : SettingsGroup("Accounts/" + accountId, qsettingsArgs...) {} QString userId() const; QUrl homeserver() const; void setHomeserver(const QUrl& url); /** \deprecated \sa setToken */ QString accessToken() const; /** \deprecated Storing accessToken in QSettings is unsafe, * see quotient-im/Quaternion#181 */ void setAccessToken(const QString& accessToken); Q_INVOKABLE void clearAccessToken(); QByteArray encryptionAccountPickle(); void setEncryptionAccountPickle(const QByteArray& encryptionAccountPickle); Q_INVOKABLE void clearEncryptionAccountPickle(); }; } // namespace Quotient spectral/include/libQuotient/lib/connectiondata.cpp0000644000175000000620000001177613566674122022565 0ustar dilingerstaff/****************************************************************************** * Copyright (C) 2015 Felix Rohrbach * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "connectiondata.h" #include "logging.h" #include "networkaccessmanager.h" #include "jobs/basejob.h" #include #include #include #include using namespace Quotient; class ConnectionData::Private { public: explicit Private(QUrl url) : baseUrl(std::move(url)) { rateLimiter.setSingleShot(true); } QUrl baseUrl; QByteArray accessToken; QString lastEvent; QString userId; QString deviceId; mutable unsigned int txnCounter = 0; const qint64 txnBase = QDateTime::currentMSecsSinceEpoch(); QString id() const { return userId + '/' + deviceId; } using job_queue_t = std::queue>; std::array jobs; // 0 - foreground, 1 - background QTimer rateLimiter; }; ConnectionData::ConnectionData(QUrl baseUrl) : d(std::make_unique(std::move(baseUrl))) { // Each lambda invocation below takes no more than one job from the // queues (first foreground, then background) and resumes it; then // restarts the rate limiter timer with duration 0, effectively yielding // to the event loop and then resuming until both queues are empty. QObject::connect(&d->rateLimiter, &QTimer::timeout, [this] { // TODO: Consider moving out all job->sendRequest() invocations to // a dedicated thread d->rateLimiter.setInterval(0); for (auto& q : d->jobs) while (!q.empty()) { auto& job = q.front(); q.pop(); if (!job || job->error() == BaseJob::Abandoned) continue; if (job->error() != BaseJob::Pending) { qCCritical(MAIN) << "Job" << job << "is in the wrong status:" << job->status(); Q_ASSERT(false); job->setStatus(BaseJob::Pending); } job->sendRequest(); d->rateLimiter.start(); return; } qCDebug(MAIN) << d->id() << "job queues are empty"; }); } ConnectionData::~ConnectionData() = default; void ConnectionData::submit(BaseJob* job) { Q_ASSERT(job->error() == BaseJob::Pending); if (!d->rateLimiter.isActive()) { job->sendRequest(); return; } d->jobs[size_t(job->isBackground())].emplace(job); qCDebug(MAIN) << job << "queued," << d->jobs.front().size() << "+" << d->jobs.back().size() << "total jobs in" << d->id() << "queues"; } void ConnectionData::limitRate(std::chrono::milliseconds nextCallAfter) { qCDebug(MAIN) << "Jobs for" << (d->userId + "/" + d->deviceId) << "suspended for" << nextCallAfter.count() << "ms"; d->rateLimiter.start(nextCallAfter); } QByteArray ConnectionData::accessToken() const { return d->accessToken; } QUrl ConnectionData::baseUrl() const { return d->baseUrl; } QNetworkAccessManager* ConnectionData::nam() const { return NetworkAccessManager::instance(); } void ConnectionData::setBaseUrl(QUrl baseUrl) { d->baseUrl = std::move(baseUrl); qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; } void ConnectionData::setToken(QByteArray token) { d->accessToken = std::move(token); } void ConnectionData::setHost(QString host) { d->baseUrl.setHost(host); qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; } void ConnectionData::setPort(int port) { d->baseUrl.setPort(port); qCDebug(MAIN) << "updated baseUrl to" << d->baseUrl; } const QString& ConnectionData::deviceId() const { return d->deviceId; } const QString& ConnectionData::userId() const { return d->userId; } void ConnectionData::setDeviceId(const QString& deviceId) { d->deviceId = deviceId; } void ConnectionData::setUserId(const QString& userId) { d->userId = userId; } QString ConnectionData::lastEvent() const { return d->lastEvent; } void ConnectionData::setLastEvent(QString identifier) { d->lastEvent = std::move(identifier); } QByteArray ConnectionData::generateTxnId() const { return d->deviceId.toLatin1() + QByteArray::number(d->txnBase) + QByteArray::number(++d->txnCounter); } spectral/include/libQuotient/3rdparty/0002755000175000000620000000000013566674122020060 5ustar dilingerstaffspectral/include/libQuotient/3rdparty/libQtOlm/0002755000175000000620000000000013566674122021603 5ustar dilingerstaffspectral/include/libQuotient/3rdparty/libQtOlm/cmake/0002755000175000000620000000000013566674122022663 5ustar dilingerstaffspectral/include/libQuotient/3rdparty/libQtOlm/cmake/QtOlmConfig.cmake0000644000175000000620000000016013566674122026042 0ustar dilingerstaffinclude(CMakeFindDependencyMacro) find_dependency(Olm) include("${CMAKE_CURRENT_LIST_DIR}/QtOlmTargets.cmake") spectral/include/libQuotient/3rdparty/libQtOlm/QtOlm.pro0000644000175000000620000000017213566674122023357 0ustar dilingerstaffTEMPLATE = app CONFIG += console object_parallel_to_source include(libQtOlm.pri) SOURCES += \ main.cpp HEADERS += spectral/include/libQuotient/3rdparty/libQtOlm/main.cpp0000644000175000000620000004655613566674122023251 0ustar dilingerstaff#include "account.h" #include "errors.h" #include "groupsession.h" #include "message.h" #include "pk.h" #include "session.h" #include "utils.h" #include #include class AccountTest : public QObject { public: explicit AccountTest(QObject* parent = nullptr) : QObject(parent) {} void testCreation(); void testPickle(); void testInvalidUnpickle(); void testPassphrasePickle(); void testWrongPassphrasePickle(); void testOneTimeKeys(); void testValidSignature(); void testInvalidSignature(); }; void AccountTest::testCreation() { QtOlm::Account* alice = new QtOlm::Account(); Q_ASSERT(!alice->curve25519IdentityKey().isEmpty() && !alice->ed25519IdentityKey().isEmpty()); } void AccountTest::testPickle() { QtOlm::Account* alice = new QtOlm::Account(); QByteArray pickle = alice->pickle(); Q_ASSERT(alice->identityKeys() == QtOlm::Account(pickle).identityKeys()); } void AccountTest::testInvalidUnpickle() { try { QtOlm::Account* alice = new QtOlm::Account(QByteArray()); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } void AccountTest::testPassphrasePickle() { QtOlm::Account* alice = new QtOlm::Account(); QString passphrase("It's a secret to everybody"); QByteArray pickle = alice->pickle(passphrase); Q_ASSERT(alice->identityKeys() == QtOlm::Account(pickle, passphrase).identityKeys()); } void AccountTest::testWrongPassphrasePickle() { QtOlm::Account* alice = new QtOlm::Account(); QString passphrase("It's a secret to everybody"); QByteArray pickle = alice->pickle(passphrase); try { QtOlm::Account* alice2 = new QtOlm::Account(pickle); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } void AccountTest::testOneTimeKeys() { QtOlm::Account* alice = new QtOlm::Account(); alice->generateOneTimeKeys(10); Q_ASSERT(!alice->oneTimeKeys().isEmpty()); Q_ASSERT(alice->curve25519OneTimeKeys().size() == 10); alice->markKeysAsPublished(); Q_ASSERT(alice->curve25519OneTimeKeys().isEmpty()); } const QString message = "This is a message."; void AccountTest::testValidSignature() { QtOlm::Account* alice = new QtOlm::Account(); QByteArray signature = alice->sign(message); QByteArray signingKey = alice->ed25519IdentityKey(); Q_ASSERT(!signature.isEmpty()); Q_ASSERT(!signingKey.isEmpty()); QtOlm::ed25519Verify(signingKey, message, signature); } void AccountTest::testInvalidSignature() { QtOlm::Account* alice = new QtOlm::Account(); QtOlm::Account* bob = new QtOlm::Account(); QByteArray signature = alice->sign(message); QByteArray signingKey = bob->ed25519IdentityKey(); Q_ASSERT(!signature.isEmpty()); Q_ASSERT(!signingKey.isEmpty()); try { QtOlm::ed25519Verify(signingKey, message, signature); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } class SessionTest : public QObject { public: explicit SessionTest(QObject* parent = nullptr) : QObject(parent) {} void testCreate(); void testPickle(); void testInvalidPickle(); void testWrongPassphrasePickle(); void testEncrypt(); void testEmptyMessage(); void testInboundWithId(); void testTwoMessages(); void testMatches(); void testInvalid(); void testDoesntMatch(); }; std::tuple createSession() { QtOlm::Account* alice = new QtOlm::Account(); QtOlm::Account* bob = new QtOlm::Account(); bob->generateOneTimeKeys(1); QByteArray idKey = bob->curve25519IdentityKey(); QByteArray oneTime = bob->curve25519OneTimeKeys().values()[0].toByteArray(); QtOlm::OutboundSession* session = new QtOlm::OutboundSession(alice, idKey, oneTime); return std::tuple( alice, bob, session); } void SessionTest::testCreate() { QtOlm::Session* session1 = std::get<2>(createSession()); QtOlm::Session* session2 = std::get<2>(createSession()); Q_ASSERT(session1); Q_ASSERT(session2); Q_ASSERT(session1->id() != session2->id()); } void SessionTest::testPickle() { QtOlm::Account *alice, *bob; QtOlm::Session* session; std::tie(alice, bob, session) = createSession(); Q_ASSERT(QtOlm::Session(session->pickle()).id() == session->id()); } void SessionTest::testInvalidPickle() { try { QtOlm::Session("", ""); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } void SessionTest::testWrongPassphrasePickle() { QtOlm::Account *alice, *bob; QtOlm::Session* session; std::tie(alice, bob, session) = createSession(); QString passphrase("It's a secret to everybody"); QByteArray pickle = alice->pickle(passphrase); try { new QtOlm::Session(pickle); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } void SessionTest::testEncrypt() { QString plainText = "It's a secret to everybody"; QtOlm::Account *alice, *bob; QtOlm::Session* session; std::tie(alice, bob, session) = createSession(); QtOlm::_Message* message = session->encrypt(plainText); Q_ASSERT(dynamic_cast(message)); QtOlm::PreKeyMessage* preKeyMessage = dynamic_cast(message); QtOlm::InboundSession* bobSession = new QtOlm::InboundSession(bob, preKeyMessage); Q_ASSERT(plainText == bobSession->decrypt(message)); } void SessionTest::testEmptyMessage() { try { QtOlm::PreKeyMessage(""); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } QtOlm::Account *alice, *bob; QtOlm::Session* session; std::tie(alice, bob, session) = createSession(); QtOlm::PreKeyMessage* empty = new QtOlm::PreKeyMessage("x"); try { session->decrypt(empty); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } void SessionTest::testInboundWithId() { QString plainText = "It's a secret to everybody"; QtOlm::Account *alice, *bob; QtOlm::Session* session; std::tie(alice, bob, session) = createSession(); QtOlm::_Message* message = session->encrypt(plainText); QByteArray aliceId = alice->curve25519IdentityKey(); QtOlm::Session* bobSession = new QtOlm::InboundSession( bob, dynamic_cast(message), aliceId); Q_ASSERT(plainText == bobSession->decrypt(message)); } void SessionTest::testTwoMessages() { QString plainText = "It's a secret to everybody"; QtOlm::Account *alice, *bob; QtOlm::Session* session; std::tie(alice, bob, session) = createSession(); QtOlm::_Message* message = session->encrypt(plainText); QByteArray aliceId = alice->curve25519IdentityKey(); QtOlm::Session* bobSession = new QtOlm::InboundSession( bob, dynamic_cast(message), aliceId); bob->removeOneTimeKeys(bobSession); Q_ASSERT(plainText == bobSession->decrypt(message)); QString bobPlainText = "Grumble, Grumble"; QtOlm::_Message* bobMessage = bobSession->encrypt(bobPlainText); Q_ASSERT(dynamic_cast(bobMessage)); Q_ASSERT(bobPlainText == session->decrypt(bobMessage)); } void SessionTest::testMatches() { QString plainText = "It's a secret to everybody"; QtOlm::Account *alice, *bob; QtOlm::Session* session; std::tie(alice, bob, session) = createSession(); QtOlm::_Message* message = session->encrypt(plainText); QByteArray aliceId = alice->curve25519IdentityKey(); QtOlm::Session* bobSession = new QtOlm::InboundSession( bob, dynamic_cast(message), aliceId); Q_ASSERT(plainText == bobSession->decrypt(message)); QtOlm::_Message* message2 = session->encrypt("Hey! Listen!"); Q_ASSERT(bobSession->matches(dynamic_cast(message2))); Q_ASSERT(bobSession->matches(dynamic_cast(message2), aliceId)); } void SessionTest::testInvalid() { QtOlm::Account *alice, *bob; QtOlm::Session* session; std::tie(alice, bob, session) = createSession(); QtOlm::PreKeyMessage* message = new QtOlm::PreKeyMessage("x"); try { session->matches(message); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } try { new QtOlm::InboundSession(bob, message); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } try { new QtOlm::OutboundSession(alice, "", "x"); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } try { new QtOlm::OutboundSession(alice, "x", ""); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } void SessionTest::testDoesntMatch() { QString plainText = "It's a secret to everybody"; QtOlm::Account *alice, *bob; QtOlm::Session* session; std::tie(alice, bob, session) = createSession(); QtOlm::_Message* message = session->encrypt(plainText); QByteArray aliceId = alice->curve25519IdentityKey(); QtOlm::Session* bobSession = new QtOlm::InboundSession( bob, dynamic_cast(message), aliceId); QtOlm::Session* newSession = std::get<2>(createSession()); QtOlm::_Message* newMessage = newSession->encrypt(plainText); Q_ASSERT( !bobSession->matches(dynamic_cast(newMessage))); } class GroupSessionTest : public QObject { public: explicit GroupSessionTest(QObject* parent = nullptr) : QObject(parent) {} void testCreate(); void testSessionID(); void testIndex(); void testOutboundPickle(); void testInvalidUnpickle(); void testInboundCreate(); void testInvalidDecrypt(); void testInboundPickle(); void testInboundExport(); void testFirstIndex(); void testEncrypt(); void testDecrypt(); void testDecryptTwice(); void testDecryptFailure(); void testID(); void testInboundFail(); void testOutboundPickleFail(); }; void GroupSessionTest::testCreate() { new QtOlm::OutboundGroupSession(); } void GroupSessionTest::testSessionID() { QtOlm::OutboundGroupSession* session = new QtOlm::OutboundGroupSession(); session->id(); } void GroupSessionTest::testIndex() { QtOlm::OutboundGroupSession* session = new QtOlm::OutboundGroupSession(); Q_ASSERT(session->messageIndex() == 0); } void GroupSessionTest::testOutboundPickle() { QtOlm::OutboundGroupSession* session = new QtOlm::OutboundGroupSession(); QByteArray pickle = session->pickle(); Q_ASSERT(session->id() == QtOlm::OutboundGroupSession(pickle).id()); } void GroupSessionTest::testInvalidUnpickle() { try { new QtOlm::OutboundGroupSession(QByteArray()); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } try { new QtOlm::InboundGroupSession(QByteArray()); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } void GroupSessionTest::testInboundCreate() { QtOlm::OutboundGroupSession* outbound = new QtOlm::OutboundGroupSession(); new QtOlm::InboundGroupSession(outbound->sessionKey()); } void GroupSessionTest::testInvalidDecrypt() { QtOlm::OutboundGroupSession* outbound = new QtOlm::OutboundGroupSession(); QtOlm::InboundGroupSession* inbound = new QtOlm::InboundGroupSession(outbound->sessionKey()); try { inbound->decrypt(""); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } void GroupSessionTest::testInboundPickle() { QtOlm::OutboundGroupSession* outbound = new QtOlm::OutboundGroupSession(); QtOlm::InboundGroupSession* inbound = new QtOlm::InboundGroupSession(outbound->sessionKey()); QByteArray pickle = inbound->pickle(); new QtOlm::InboundGroupSession(pickle); } void GroupSessionTest::testInboundExport() { QtOlm::OutboundGroupSession* outbound = new QtOlm::OutboundGroupSession(); QtOlm::InboundGroupSession* inbound = new QtOlm::InboundGroupSession(outbound->sessionKey()); QtOlm::InboundGroupSession* imported = new QtOlm::InboundGroupSession( inbound->exportSession(inbound->firstKnownIndex()), QtOlm::InboundGroupSession::Initialization::Import); QString result; int index; std::tie(result, index) = (imported->decrypt(outbound->encrypt("Test"))); Q_ASSERT(result == "Test"); Q_ASSERT(index == 0); } void GroupSessionTest::testFirstIndex() { QtOlm::OutboundGroupSession* outbound = new QtOlm::OutboundGroupSession(); QtOlm::InboundGroupSession* inbound = new QtOlm::InboundGroupSession(outbound->sessionKey()); int index = inbound->firstKnownIndex(); } void GroupSessionTest::testEncrypt() { QtOlm::OutboundGroupSession* outbound = new QtOlm::OutboundGroupSession(); QtOlm::InboundGroupSession* inbound = new QtOlm::InboundGroupSession(outbound->sessionKey()); int index; QString decryptedText; std::tie(decryptedText, index) = inbound->decrypt(outbound->encrypt("Test")); Q_ASSERT(decryptedText == "Test"); Q_ASSERT(index == 0); } void GroupSessionTest::testDecrypt() { QtOlm::OutboundGroupSession* outbound = new QtOlm::OutboundGroupSession(); QtOlm::InboundGroupSession* inbound = new QtOlm::InboundGroupSession(outbound->sessionKey()); int index; QString decryptedText; std::tie(decryptedText, index) = inbound->decrypt(outbound->encrypt("Test")); Q_ASSERT(decryptedText == "Test"); Q_ASSERT(index == 0); } void GroupSessionTest::testDecryptTwice() { QtOlm::OutboundGroupSession* outbound = new QtOlm::OutboundGroupSession(); QtOlm::InboundGroupSession* inbound = new QtOlm::InboundGroupSession(outbound->sessionKey()); outbound->encrypt("Test 1"); int index; QString decryptedText; std::tie(decryptedText, index) = inbound->decrypt(outbound->encrypt("Test 2")); Q_ASSERT(decryptedText == "Test 2"); Q_ASSERT(index == 1); } void GroupSessionTest::testDecryptFailure() { QtOlm::OutboundGroupSession* outbound = new QtOlm::OutboundGroupSession(); QtOlm::InboundGroupSession* inbound = new QtOlm::InboundGroupSession(outbound->sessionKey()); QtOlm::OutboundGroupSession* eveOutbound = new QtOlm::OutboundGroupSession(); try { inbound->decrypt(eveOutbound->encrypt("Test")); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } void GroupSessionTest::testID() { QtOlm::OutboundGroupSession* outbound = new QtOlm::OutboundGroupSession(); QtOlm::InboundGroupSession* inbound = new QtOlm::InboundGroupSession(outbound->sessionKey()); Q_ASSERT(outbound->id() == inbound->id()); } void GroupSessionTest::testInboundFail() { try { new QtOlm::InboundGroupSession(""); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } void GroupSessionTest::testOutboundPickleFail() { QtOlm::OutboundGroupSession* outbound = new QtOlm::OutboundGroupSession(); QByteArray pickle = outbound->pickle("Test"); try { new QtOlm::OutboundGroupSession(pickle, ""); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } class PkTest : public QObject { public: explicit PkTest(QObject* parent = nullptr) : QObject(parent) {} void testInvalidEncryption(); void testDecryption(); void testInvalidDecryption(); void testPickling(); void testInvalidUnpickling(); void testInvalidPassPickling(); }; void PkTest::testInvalidEncryption() { try { new QtOlm::PkEncryption(""); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } void PkTest::testDecryption() { QtOlm::PkDecryption decryption; QtOlm::PkEncryption encryption(decryption.publicKey()); QString plainText = "It's a secret to everybody."; auto message = encryption.encrypt(plainText); auto decryptedPlainText = decryption.decrypt(message); Q_ASSERT(plainText == decryptedPlainText); } void PkTest::testInvalidDecryption() { QtOlm::PkDecryption decryption; QtOlm::PkEncryption encryption(decryption.publicKey()); QString plainText = "It's a secret to everybody."; auto message = encryption.encrypt(plainText); QtOlm::PkMessage fakeMessage(QByteArray("?"), message->mac(), message->cipherText(), message->parent()); try { decryption.decrypt(&fakeMessage); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } void PkTest::testPickling() { QtOlm::PkDecryption decryption; QtOlm::PkEncryption encryption(decryption.publicKey()); QString plainText = "It's a secret to everybody."; auto message = encryption.encrypt(plainText); QByteArray pickle = decryption.pickle(); QtOlm::PkDecryption unpickled(pickle, ""); auto decryptedPlainText = unpickled.decrypt(message); Q_ASSERT(plainText == decryptedPlainText); } void PkTest::testInvalidUnpickling() { try { new QtOlm::PkDecryption("", ""); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } void PkTest::testInvalidPassPickling() { QtOlm::PkDecryption decryption; QtOlm::PkEncryption encryption(decryption.publicKey()); QString plainText = "It's a secret to everybody."; auto message = encryption.encrypt(plainText); QByteArray pickle = decryption.pickle("Secret"); try { QtOlm::PkDecryption unpickled(pickle, ""); throw new QtOlm::UnknownError(); } catch (std::exception* e) { Q_ASSERT(dynamic_cast(e)); } } int main() { AccountTest* accountTest = new AccountTest(); accountTest->testCreation(); accountTest->testPickle(); accountTest->testInvalidUnpickle(); accountTest->testPassphrasePickle(); accountTest->testWrongPassphrasePickle(); accountTest->testOneTimeKeys(); accountTest->testValidSignature(); accountTest->testInvalidSignature(); SessionTest* sessionTest = new SessionTest(); sessionTest->testCreate(); sessionTest->testPickle(); sessionTest->testInvalidPickle(); sessionTest->testWrongPassphrasePickle(); sessionTest->testEncrypt(); sessionTest->testEmptyMessage(); sessionTest->testInboundWithId(); sessionTest->testTwoMessages(); sessionTest->testMatches(); sessionTest->testInvalid(); sessionTest->testDoesntMatch(); GroupSessionTest* groupSessionTest = new GroupSessionTest(); groupSessionTest->testCreate(); groupSessionTest->testSessionID(); groupSessionTest->testIndex(); groupSessionTest->testOutboundPickle(); groupSessionTest->testInvalidUnpickle(); groupSessionTest->testInboundCreate(); groupSessionTest->testInboundExport(); groupSessionTest->testFirstIndex(); groupSessionTest->testEncrypt(); groupSessionTest->testDecrypt(); groupSessionTest->testDecryptTwice(); groupSessionTest->testDecryptFailure(); groupSessionTest->testID(); groupSessionTest->testInboundFail(); groupSessionTest->testOutboundPickleFail(); PkTest* pkTest = new PkTest(); pkTest->testInvalidEncryption(); pkTest->testDecryption(); pkTest->testInvalidDecryption(); pkTest->testPickling(); pkTest->testInvalidUnpickling(); pkTest->testInvalidPassPickling(); return 0; } spectral/include/libQuotient/3rdparty/libQtOlm/.git0000644000175000000620000000011713566674122022364 0ustar dilingerstaffgitdir: ../../../../.git/modules/include/libQuotient/modules/3rdparty/libQtOlm spectral/include/libQuotient/3rdparty/libQtOlm/libQtOlm.pri0000644000175000000620000000105113566674122024035 0ustar dilingerstaffQT += core network CONFIG += c++14 object_parallel_to_source SRCPATH = $$PWD/lib INCLUDEPATH += $$SRCPATH mac { INCLUDEPATH += /usr/local/include LIBS += -L/usr/local/lib } unix|win32: LIBS += -lolm HEADERS += \ $$SRCPATH/account.h \ $$SRCPATH/utils.h \ $$SRCPATH/session.h \ $$SRCPATH/message.h \ $$SRCPATH/errors.h \ $$SRCPATH/groupsession.h \ $$SRCPATH/pk.h SOURCES += \ $$SRCPATH/account.cpp \ $$SRCPATH/session.cpp \ $$SRCPATH/message.cpp \ $$SRCPATH/groupsession.cpp \ $$SRCPATH/pk.cpp spectral/include/libQuotient/3rdparty/libQtOlm/CMakeLists.txt0000644000175000000620000001113213566674122024337 0ustar dilingerstaffcmake_minimum_required(VERSION 3.1) set(API_VERSION "0.1") project(qtolm VERSION "${API_VERSION}.0" LANGUAGES CXX) include(CheckCXXCompilerFlag) if (NOT WIN32) include(GNUInstallDirs) endif(NOT WIN32) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) # Set a default build type if none was specified if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to 'Debug' as none was specified") set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build" FORCE) # Set the possible values of build type for cmake-gui set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() if (NOT CMAKE_INSTALL_LIBDIR) set(CMAKE_INSTALL_LIBDIR "lib") endif() if (NOT CMAKE_INSTALL_BINDIR) set(CMAKE_INSTALL_BINDIR "bin") endif() if (NOT CMAKE_INSTALL_INCLUDEDIR) set(CMAKE_INSTALL_INCLUDEDIR "include") endif() foreach (FLAG all "" pedantic extra error=return-type no-unused-parameter no-gnu-zero-variadic-macro-arguments) CHECK_CXX_COMPILER_FLAG("-W${FLAG}" WARN_${FLAG}_SUPPORTED) if ( WARN_${FLAG}_SUPPORTED AND NOT CMAKE_CXX_FLAGS MATCHES "(^| )-W?${FLAG}($| )") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W${FLAG}") endif () endforeach () find_package(Qt5 5.9 REQUIRED Network) # olm find_package(Olm 3.0.0 REQUIRED) message( STATUS ) message( STATUS "=============================================================================" ) message( STATUS " libqtolm Build Information" ) message( STATUS "=============================================================================" ) message( STATUS "Version: ${PROJECT_VERSION}, API version: ${API_VERSION}") if (CMAKE_BUILD_TYPE) message( STATUS "Build type: ${CMAKE_BUILD_TYPE}") endif(CMAKE_BUILD_TYPE) message( STATUS "Using compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) message( STATUS "Using Qt ${Qt5_VERSION}" ) message( STATUS "=============================================================================" ) message( STATUS ) # Set up source files set(libqtolm_SRCS lib/account.cpp lib/account.h lib/groupsession.cpp lib/groupsession.h lib/message.cpp lib/message.h lib/pk.cpp lib/pk.h lib/session.cpp lib/session.h lib/utils.h lib/errors.h ) add_library(QtOlm ${libqtolm_SRCS}) set_property(TARGET QtOlm PROPERTY VERSION "${PROJECT_VERSION}") set_property(TARGET QtOlm PROPERTY SOVERSION ${API_VERSION} ) set_property(TARGET QtOlm PROPERTY INTERFACE_QtOlm_MAJOR_VERSION ${API_VERSION}) set_property(TARGET QtOlm APPEND PROPERTY COMPATIBLE_INTERFACE_STRING QtOlm_MAJOR_VERSION) target_compile_features(QtOlm PUBLIC cxx_std_14) target_include_directories(QtOlm PUBLIC $ # $ ) target_link_libraries(QtOlm Olm::Olm Qt5::Core Qt5::Network) configure_file(QtOlm.pc.in ${CMAKE_CURRENT_BINARY_DIR}/QtOlm.pc @ONLY NEWLINE_STYLE UNIX) # Installation install(TARGETS QtOlm EXPORT QtOlmTargets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) install(DIRECTORY lib/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING PATTERN "*.h") include(CMakePackageConfigHelpers) # NB: SameMajorVersion doesn't really work yet, as we're within 0.x trail. # Maybe consider jumping the gun and releasing 1.0, as semver advises? write_basic_package_version_file( "${CMAKE_CURRENT_BINARY_DIR}/QtOlm/QtOlmConfigVersion.cmake" COMPATIBILITY SameMajorVersion ) export(PACKAGE QtOlm) export(EXPORT QtOlmTargets FILE "${CMAKE_CURRENT_BINARY_DIR}/QtOlm/QtOlmTargets.cmake") configure_file(cmake/QtOlmConfig.cmake "${CMAKE_CURRENT_BINARY_DIR}/QtOlm/QtOlmConfig.cmake" COPYONLY ) set(ConfigFilesLocation "${CMAKE_INSTALL_LIBDIR}/cmake/QtOlm") install(EXPORT QtOlmTargets FILE QtOlmTargets.cmake DESTINATION ${ConfigFilesLocation}) install(FILES cmake/QtOlmConfig.cmake "${CMAKE_CURRENT_BINARY_DIR}/QtOlm/QtOlmConfigVersion.cmake" DESTINATION ${ConfigFilesLocation} ) # Only available from CMake 3.7; reserved for future use #install(EXPORT_ANDROID_MK QtOlmTargets DESTINATION share/ndk-modules) if (WIN32) install(FILES mime/packages/freedesktop.org.xml DESTINATION mime/packages) endif (WIN32) if (UNIX AND NOT APPLE) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/QtOlm.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) endif() spectral/include/libQuotient/3rdparty/libQtOlm/.gitlab-ci.yml0000644000175000000620000000047213566674122024240 0ustar dilingerstaffimage: registry.gitlab.com/b0/qt-olm-docker stages: - build - test build: stage: build script: - /opt/qt512/bin/qt512-env.sh - mkdir build && cd build - /opt/qt512/bin/qmake .. - make - cd ../ artifacts: paths: - build test: stage: test script: - ./build/QtOlm spectral/include/libQuotient/3rdparty/libQtOlm/QtOlm.pc.in0000644000175000000620000000040713566674122023567 0ustar dilingerstaffprefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@ Name: QtOlm Description: A Qt wrapper for libolm Version: @API_VERSION@ Cflags: -I${includedir} Libs: -L${libdir} -lQtOlm spectral/include/libQuotient/3rdparty/libQtOlm/lib/0002755000175000000620000000000013566674122022351 5ustar dilingerstaffspectral/include/libQuotient/3rdparty/libQtOlm/lib/groupsession.h0000644000175000000620000000347013566674122025264 0ustar dilingerstaff#ifndef GROUPSESSION_H #define GROUPSESSION_H #include "olm/olm.h" #include #include namespace QtOlm { class InboundGroupSession : public QObject { Q_OBJECT Q_PROPERTY(QString id READ id) Q_PROPERTY(int firstKnownIndex READ firstKnownIndex) public: enum Initialization { Init, Import, }; explicit InboundGroupSession(QByteArray sessionKey, Initialization type = Initialization::Init, QObject* parent = nullptr); InboundGroupSession(QByteArray sessionKey, QByteArray pickle, QString passphrase = "", QObject* parent = nullptr); ~InboundGroupSession(); QByteArray pickle(QString passphrase = ""); std::pair decrypt(QByteArray cipherText); QString id(); int firstKnownIndex(); QByteArray exportSession(int messageIndex); private: static OlmInboundGroupSession* newSession(); protected: void checkErr(size_t code); OlmInboundGroupSession* _session; }; class OutboundGroupSession : public QObject { Q_OBJECT Q_PROPERTY(QString id READ id) Q_PROPERTY(int messageIndex READ messageIndex) Q_PROPERTY(QByteArray sessionKey READ sessionKey) public: explicit OutboundGroupSession(QObject* parent = nullptr); OutboundGroupSession(QByteArray pickle, QString passphrase = "", QObject* parent = nullptr); ~OutboundGroupSession(); QByteArray pickle(QString passphrase = ""); QByteArray encrypt(QString plainText); QString id(); int messageIndex(); QByteArray sessionKey(); private: static OlmOutboundGroupSession* newSession(); protected: void checkErr(size_t code); OlmOutboundGroupSession* _session; }; } // namespace QtOlm #endif // GROUPSESSION_H spectral/include/libQuotient/3rdparty/libQtOlm/lib/session.cpp0000644000175000000620000001451413566674122024543 0ustar dilingerstaff#include "session.h" #include "errors.h" #include "message.h" #include "utils.h" #include using namespace QtOlm; Session::Session(QObject* parent) : QObject(parent), _session(newSession()) {} Session::Session(QByteArray pickle, QString passphrase, QObject* parent) : QObject(parent), _session(newSession()) { if (pickle.isEmpty()) throw new InvalidArgument("Pickle is empty"); std::string pass = passphrase.toStdString(); checkErr(olm_unpickle_session(_session, pass.data(), pass.length(), pickle.data(), pickle.length())); } Session::~Session() { olm_clear_session(_session); } QByteArray Session::pickle(QString passphrase) { std::string pass = passphrase.toStdString(); size_t pickleLength = olm_pickle_session_length(_session); QByteArray pickleBuffer(pickleLength, '0'); checkErr(olm_pickle_session(_session, pass.data(), pass.length(), pickleBuffer.data(), pickleLength)); return pickleBuffer; } _Message* Session::encrypt(QString plainText) { std::string plainTxt = plainText.toStdString(); size_t randomLength = olm_encrypt_random_length(_session); QByteArray randomBuffer = getRandomData(randomLength); size_t messageType = olm_encrypt_message_type(_session); checkErr(messageType); size_t cipherTextLength = olm_encrypt_message_length(_session, plainTxt.length()); QByteArray cipherTextBuffer(cipherTextLength, '0'); checkErr(olm_encrypt(_session, plainTxt.data(), plainTxt.length(), randomBuffer.data(), randomLength, cipherTextBuffer.data(), cipherTextLength)); if (messageType == OLM_MESSAGE_TYPE_PRE_KEY) return new PreKeyMessage(cipherTextBuffer); else if (messageType == OLM_MESSAGE_TYPE_MESSAGE) return new Message(cipherTextBuffer); throw new InvalidArgument("Unknown message type"); } QString Session::decrypt(_Message* message) { if (message->cipherText().isEmpty()) throw new InvalidArgument("Message is empty"); QByteArray cipherTextBuffer = message->cipherText(); size_t maxPlainTextLength = olm_decrypt_max_plaintext_length( _session, message->messageType(), cipherTextBuffer.data(), cipherTextBuffer.length()); cipherTextBuffer = message->cipherText(); // Invoking olm_decrypt_max_plaintext_length // changes cipherTextBuffer's content QByteArray plainTextBuffer(maxPlainTextLength, '0'); size_t plainTextLength = olm_decrypt( _session, message->messageType(), cipherTextBuffer.data(), cipherTextBuffer.length(), plainTextBuffer.data(), maxPlainTextLength); checkErr(plainTextLength); plainTextBuffer.truncate(plainTextLength); return QString(plainTextBuffer); } bool Session::matches(PreKeyMessage* message, QString identityKey) { if (message->cipherText().isEmpty()) throw new InvalidArgument("Cipher text is empty"); QByteArray messageBuffer = message->cipherText(); size_t ret; if (identityKey.isEmpty()) { ret = olm_matches_inbound_session(_session, messageBuffer.data(), messageBuffer.length()); } else { QByteArray identityKeyBuffer(identityKey.toUtf8()); ret = olm_matches_inbound_session_from( _session, identityKeyBuffer.data(), identityKeyBuffer.length(), messageBuffer.data(), messageBuffer.length()); } checkErr(ret); return bool(ret); } QString Session::id() { size_t idLength = olm_session_id_length(_session); QByteArray idBuffer(idLength, '0'); checkErr(olm_session_id(_session, idBuffer.data(), idLength)); return idBuffer; } void Session::checkErr(size_t code) { if (code != olm_error()) return; std::string lastError = olm_session_last_error(_session); if (lastError == "SUCCESS") return; if (lastError == "NOT_ENOUGH_RANDOM") throw new EntropyError(lastError); if (lastError == "OUTPUT_BUFFER_TOO_SMALL" || lastError == "OLM_INPUT_BUFFER_TOO_SMALL") throw new BufferError(lastError); if (lastError == "BAD_MESSAGE_VERSION" || lastError == "BAD_MESSAGE_FORMAT" || lastError == "BAD_MESSAGE_MAC" || lastError == "BAD_MESSAGE_KEY_ID" || lastError == "UNKNOWN_MESSAGE_INDEX") throw new MessageError(lastError); if (lastError == "INVALID_BASE64") throw new Base64Error(lastError); if (lastError == "BAD_ACCOUNT_KEY") throw new AccountKeyError(lastError); if (lastError == "UNKNOWN_PICKLE_VERSION" || lastError == "CORRUPTED_PICKLE" || lastError == "BAD_LEGACY_ACCOUNT_PICKLE") throw new PickleError(lastError); if (lastError == "BAD_SESSION_KEY") throw new SessionKeyError(lastError); if (lastError == "BAD_SIGNATURE") throw new SignatureError(lastError); throw new OlmError(lastError); } OlmSession* Session::newSession() { return olm_session(new uint8_t[olm_session_size()]); } InboundSession::InboundSession(Account* account, PreKeyMessage* message, QByteArray identityKey, QObject* parent) : Session(parent) { if (message->cipherText().isEmpty()) throw new InvalidArgument("Ciphertext is empty"); QByteArray messageBuffer = message->cipherText(); if (identityKey.isEmpty()) { checkErr(olm_create_inbound_session(_session, account->account(), messageBuffer.data(), messageBuffer.length())); } else { checkErr(olm_create_inbound_session_from( _session, account->account(), identityKey.data(), identityKey.length(), messageBuffer.data(), messageBuffer.length())); } } OutboundSession::OutboundSession(Account* account, QByteArray identityKey, QByteArray oneTimeKey, QObject* parent) : Session(parent) { if (identityKey.isEmpty()) throw new InvalidArgument("Identity key is empty"); if (oneTimeKey.isEmpty()) throw new InvalidArgument("One time key is empty"); size_t sessionRandomLength = olm_create_outbound_session_random_length(_session); QByteArray randomBuffer = getRandomData(sessionRandomLength); checkErr(olm_create_outbound_session( _session, account->account(), identityKey.data(), identityKey.length(), oneTimeKey.data(), oneTimeKey.length(), randomBuffer.data(), sessionRandomLength)); } spectral/include/libQuotient/3rdparty/libQtOlm/lib/pk.h0000644000175000000620000000321213566674122023130 0ustar dilingerstaff#ifndef PK_H #define PK_H #include #include "olm/olm.h" #include "olm/pk.h" namespace QtOlm { class PkMessage : public QObject { Q_OBJECT Q_PROPERTY(QByteArray ephermalKey READ ephermalKey) Q_PROPERTY(QByteArray mac READ mac) Q_PROPERTY(QByteArray cipherText READ cipherText) public: explicit PkMessage(QByteArray ephermalKey, QByteArray mac, QByteArray cipherText, QObject* parent = nullptr); virtual ~PkMessage() {} QByteArray ephermalKey() { return _ephermalKey; } QByteArray mac() { return _mac; } QByteArray cipherText() { return _cipherText; } private: QByteArray _ephermalKey; QByteArray _mac; QByteArray _cipherText; }; class PkEncryption : public QObject { Q_OBJECT public: explicit PkEncryption(QByteArray recipientKey, QObject* parent = nullptr); ~PkEncryption(); PkMessage* encrypt(QString plainText); private: static OlmPkEncryption* newPkEncryption(); void checkErr(size_t code); OlmPkEncryption* _pkEncryption; }; class PkDecryption : public QObject { Q_OBJECT Q_PROPERTY(QByteArray publicKey READ publicKey) public: explicit PkDecryption(QObject* parent = nullptr); PkDecryption(QByteArray pickle, QString passphrase = "", QObject* parent = nullptr); ~PkDecryption(); QByteArray pickle(QString passphrase = ""); QString decrypt(PkMessage* message); QByteArray publicKey() { return _publicKey; } private: static OlmPkDecryption* newPkDecryption(); void checkErr(size_t code); OlmPkDecryption* _pkDecryption; QByteArray _publicKey; }; } // namespace QtOlm #endif // PK_H spectral/include/libQuotient/3rdparty/libQtOlm/lib/message.h0000644000175000000620000000167613566674122024156 0ustar dilingerstaff#ifndef MESSAGE_H #define MESSAGE_H #include "olm/olm.h" #include #include #include namespace QtOlm { class _Message : public QObject { Q_OBJECT Q_PROPERTY(size_t messageType READ messageType) Q_PROPERTY(QByteArray cipherText READ cipherText) public: explicit _Message(QByteArray cipher, size_t type, QObject* parent = nullptr); size_t messageType() { return _messageType; } QByteArray cipherText() { return _cipherText; } private: size_t _messageType; QByteArray _cipherText; }; class Message : public _Message { Q_OBJECT public: explicit Message(QByteArray cipher, QObject* parent = nullptr) : _Message(cipher, OLM_MESSAGE_TYPE_MESSAGE, parent) {} }; class PreKeyMessage : public _Message { Q_OBJECT public: explicit PreKeyMessage(QByteArray cipher, QObject* parent = nullptr) : _Message(cipher, OLM_MESSAGE_TYPE_PRE_KEY, parent) {} }; } // namespace QtOlm #endif // MESSAGE_H spectral/include/libQuotient/3rdparty/libQtOlm/lib/errors.h0000644000175000000620000000207213566674122024035 0ustar dilingerstaff#ifndef ERRORS_H #define ERRORS_H #include #include namespace QtOlm { class UnknownError : public std::exception { public: using std::exception::exception; }; class InvalidArgument : public std::invalid_argument { public: using std::invalid_argument::invalid_argument; }; class OlmError : public std::runtime_error { public: using std::runtime_error::runtime_error; }; class EntropyError : public OlmError { public: using OlmError::OlmError; }; class BufferError : public OlmError { public: using OlmError::OlmError; }; class Base64Error : public OlmError { public: using OlmError::OlmError; }; class SignatureError : public OlmError { public: using OlmError::OlmError; }; class AccountKeyError : public OlmError { public: using OlmError::OlmError; }; class SessionKeyError : public OlmError { public: using OlmError::OlmError; }; class MessageError : public OlmError { public: using OlmError::OlmError; }; class PickleError : public OlmError { public: using OlmError::OlmError; }; } // namespace QtOlm #endif // ERRORS_H spectral/include/libQuotient/3rdparty/libQtOlm/lib/groupsession.cpp0000644000175000000620000002074113566674122025617 0ustar dilingerstaff#include "groupsession.h" #include "errors.h" #include "utils.h" #include using namespace QtOlm; InboundGroupSession::InboundGroupSession(QByteArray sessionKey, Initialization type, QObject* parent) : QObject(parent), _session(newSession()) { if (sessionKey.isEmpty()) throw new InvalidArgument("Session key is empty"); if (type == Initialization::Init) { checkErr(olm_init_inbound_group_session( _session, reinterpret_cast(sessionKey.data()), sessionKey.length())); } else if (type == Initialization::Import) { checkErr(olm_import_inbound_group_session( _session, reinterpret_cast(sessionKey.data()), sessionKey.length())); } } InboundGroupSession::InboundGroupSession(QByteArray sessionKey, QByteArray pickle, QString passphrase, QObject* parent) : InboundGroupSession(sessionKey, Initialization::Init, parent) { if (pickle.isEmpty()) throw new InvalidArgument("Pickle is empty"); std::string pass = passphrase.toStdString(); checkErr(olm_unpickle_inbound_group_session( _session, pass.data(), pass.length(), pickle.data(), pickle.length())); } InboundGroupSession::~InboundGroupSession() { olm_clear_inbound_group_session(_session); } QByteArray InboundGroupSession::pickle(QString passphrase) { std::string pass = passphrase.toStdString(); size_t pickleLength = olm_pickle_inbound_group_session_length(_session); QByteArray pickleBuffer(pickleLength, '0'); checkErr(olm_pickle_inbound_group_session( _session, pass.data(), pass.length(), pickleBuffer.data(), pickleLength)); return pickleBuffer; } std::pair InboundGroupSession::decrypt( QByteArray cipherText) { if (cipherText.isEmpty()) throw new InvalidArgument("Message is empty"); QByteArray cipherTextBuffer = cipherText; size_t maxPlainTextLength = olm_group_decrypt_max_plaintext_length( _session, reinterpret_cast(cipherTextBuffer.data()), cipherTextBuffer.length()); QByteArray plainTextBuffer(maxPlainTextLength, '0'); cipherTextBuffer = cipherText; uint32_t messageIndex; size_t plainTextLength = olm_group_decrypt( _session, reinterpret_cast(cipherTextBuffer.data()), cipherTextBuffer.length(), reinterpret_cast(plainTextBuffer.data()), maxPlainTextLength, &messageIndex); checkErr(plainTextLength); plainTextBuffer.truncate(plainTextLength); return std::pair(QString(plainTextBuffer), messageIndex); } QString InboundGroupSession::id() { size_t idLength = olm_inbound_group_session_id_length(_session); QByteArray idBuffer(idLength, '0'); checkErr(olm_inbound_group_session_id( _session, reinterpret_cast(idBuffer.data()), idLength)); return idBuffer; } int InboundGroupSession::firstKnownIndex() { return olm_inbound_group_session_first_known_index(_session); } QByteArray InboundGroupSession::exportSession(int messageIndex) { size_t exportLength = olm_export_inbound_group_session_length(_session); QByteArray exportBuffer(exportLength, '0'); checkErr(olm_export_inbound_group_session( _session, reinterpret_cast(exportBuffer.data()), exportLength, messageIndex)); return exportBuffer; } OlmInboundGroupSession* InboundGroupSession::newSession() { return olm_inbound_group_session( new uint8_t[olm_inbound_group_session_size()]); } void InboundGroupSession::checkErr(size_t code) { if (code != olm_error()) return; std::string lastError = olm_inbound_group_session_last_error(_session); if (lastError == "SUCCESS") return; if (lastError == "NOT_ENOUGH_RANDOM") throw new EntropyError(lastError); if (lastError == "OUTPUT_BUFFER_TOO_SMALL" || lastError == "OLM_INPUT_BUFFER_TOO_SMALL") throw new BufferError(lastError); if (lastError == "BAD_MESSAGE_VERSION" || lastError == "BAD_MESSAGE_FORMAT" || lastError == "BAD_MESSAGE_MAC" || lastError == "BAD_MESSAGE_KEY_ID" || lastError == "UNKNOWN_MESSAGE_INDEX") throw new MessageError(lastError); if (lastError == "INVALID_BASE64") throw new Base64Error(lastError); if (lastError == "BAD_ACCOUNT_KEY") throw new AccountKeyError(lastError); if (lastError == "UNKNOWN_PICKLE_VERSION" || lastError == "CORRUPTED_PICKLE" || lastError == "BAD_LEGACY_ACCOUNT_PICKLE") throw new PickleError(lastError); if (lastError == "BAD_SESSION_KEY") throw new SessionKeyError(lastError); if (lastError == "BAD_SIGNATURE") throw new SignatureError(lastError); throw new OlmError(lastError); } OutboundGroupSession::OutboundGroupSession(QObject* parent) : QObject(parent), _session(newSession()) { size_t randomLength = olm_init_outbound_group_session_random_length(_session); QByteArray randomBuffer = getRandomData(randomLength); checkErr(olm_init_outbound_group_session( _session, reinterpret_cast(randomBuffer.data()), randomLength)); } OutboundGroupSession::OutboundGroupSession(QByteArray pickle, QString passphrase, QObject* parent) : OutboundGroupSession(parent) { if (pickle.isEmpty()) throw new InvalidArgument("Pickle is empty"); std::string pass = passphrase.toStdString(); checkErr(olm_unpickle_outbound_group_session( _session, pass.data(), pass.length(), pickle.data(), pickle.length())); } OutboundGroupSession::~OutboundGroupSession() { olm_clear_outbound_group_session(_session); } QByteArray OutboundGroupSession::pickle(QString passphrase) { std::string pass = passphrase.toStdString(); size_t pickleLength = olm_pickle_outbound_group_session_length(_session); QByteArray pickleBuffer(pickleLength, '0'); checkErr(olm_pickle_outbound_group_session( _session, pass.data(), pass.length(), pickleBuffer.data(), pickleLength)); return pickleBuffer; } QByteArray OutboundGroupSession::encrypt(QString plainText) { std::string plainTxt = plainText.toStdString(); size_t messageLength = olm_group_encrypt_message_length(_session, plainTxt.length()); QByteArray messageBuffer(messageLength, '0'); checkErr(olm_group_encrypt( _session, reinterpret_cast(plainTxt.data()), plainTxt.length(), reinterpret_cast(messageBuffer.data()), messageLength)); return messageBuffer; } QString OutboundGroupSession::id() { size_t idLength = olm_outbound_group_session_id_length(_session); QByteArray idBuffer(idLength, '0'); checkErr(olm_outbound_group_session_id( _session, reinterpret_cast(idBuffer.data()), idLength)); return idBuffer; } int OutboundGroupSession::messageIndex() { return olm_outbound_group_session_message_index(_session); } QByteArray OutboundGroupSession::sessionKey() { size_t keyLength = olm_outbound_group_session_key_length(_session); QByteArray keyBuffer(keyLength, '0'); checkErr(olm_outbound_group_session_key( _session, reinterpret_cast(keyBuffer.data()), keyLength)); return keyBuffer; } OlmOutboundGroupSession* OutboundGroupSession::newSession() { return olm_outbound_group_session( new uint8_t[olm_outbound_group_session_size()]); } void OutboundGroupSession::checkErr(size_t code) { if (code != olm_error()) return; std::string lastError = olm_outbound_group_session_last_error(_session); if (lastError == "SUCCESS") return; if (lastError == "NOT_ENOUGH_RANDOM") throw new EntropyError(lastError); if (lastError == "OUTPUT_BUFFER_TOO_SMALL" || lastError == "OLM_INPUT_BUFFER_TOO_SMALL") throw new BufferError(lastError); if (lastError == "BAD_MESSAGE_VERSION" || lastError == "BAD_MESSAGE_FORMAT" || lastError == "BAD_MESSAGE_MAC" || lastError == "BAD_MESSAGE_KEY_ID" || lastError == "UNKNOWN_MESSAGE_INDEX") throw new MessageError(lastError); if (lastError == "INVALID_BASE64") throw new Base64Error(lastError); if (lastError == "BAD_ACCOUNT_KEY") throw new AccountKeyError(lastError); if (lastError == "UNKNOWN_PICKLE_VERSION" || lastError == "CORRUPTED_PICKLE" || lastError == "BAD_LEGACY_ACCOUNT_PICKLE") throw new PickleError(lastError); if (lastError == "BAD_SESSION_KEY") throw new SessionKeyError(lastError); if (lastError == "BAD_SIGNATURE") throw new SignatureError(lastError); throw new OlmError(lastError); } spectral/include/libQuotient/3rdparty/libQtOlm/lib/account.cpp0000644000175000000620000001115213566674122024507 0ustar dilingerstaff#include "account.h" #include "errors.h" #include "session.h" #include "utils.h" #include #include #include #include using namespace QtOlm; Account::Account(QObject* parent) : QObject(parent), _account(newAccount()) { size_t randomSize = olm_create_account_random_length(_account); QByteArray randomData = getRandomData(randomSize); checkErr(olm_create_account(_account, randomData.data(), randomSize)); } Account::Account(QByteArray pickle, QString passphrase, QObject* parent) : QObject(parent), _account(newAccount()) { if (pickle.isEmpty()) throw new InvalidArgument("Pickle is empty"); std::string pass = passphrase.toStdString(); checkErr(olm_unpickle_account(_account, pass.data(), pass.length(), pickle.data(), pickle.length())); } Account::~Account() { olm_clear_account(_account); } QByteArray Account::pickle(QString passphrase) { std::string pass = passphrase.toStdString(); size_t pickleLength = olm_pickle_account_length(_account); QByteArray pickleBuffer(pickleLength, '0'); checkErr(olm_pickle_account(_account, pass.data(), pass.length(), pickleBuffer.data(), pickleLength)); return pickleBuffer; } QPair Account::identityKeys() { size_t keyLength = olm_account_identity_keys_length(_account); QByteArray keyBuffer(keyLength, '0'); checkErr(olm_account_identity_keys(_account, keyBuffer.data(), keyLength)); QJsonObject key = QJsonDocument::fromJson(keyBuffer).object(); return QPair( key.value("curve25519").toString().toUtf8(), key.value("ed25519").toString().toUtf8()); } QByteArray Account::curve25519IdentityKey() { return identityKeys().first; } QByteArray Account::ed25519IdentityKey() { return identityKeys().second; } QByteArray Account::sign(QString message) { std::string msg = message.toStdString(); size_t sigLength = olm_account_signature_length(_account); QByteArray sigBuffer(sigLength, '0'); checkErr(olm_account_sign(_account, msg.data(), msg.length(), sigBuffer.data(), sigLength)); return sigBuffer; } QByteArray Account::sign(QJsonObject message) { return sign(QJsonDocument(message).toJson(QJsonDocument::Compact)); } int Account::maxOneTimeKeys() { return int(olm_account_max_number_of_one_time_keys(_account)); } void Account::markKeysAsPublished() { olm_account_mark_keys_as_published(_account); } void Account::generateOneTimeKeys(int count) { size_t randomLength = olm_account_generate_one_time_keys_random_length(_account, count); QByteArray randomBuffer = getRandomData(randomLength); checkErr(olm_account_generate_one_time_keys( _account, count, randomBuffer.data(), randomLength)); } QJsonObject Account::oneTimeKeys() { size_t keyLength = olm_account_one_time_keys_length(_account); QByteArray keyBuffer(keyLength, '0'); checkErr(olm_account_one_time_keys(_account, keyBuffer.data(), keyLength)); return QJsonDocument::fromJson(keyBuffer).object(); } QVariantHash Account::curve25519OneTimeKeys() { return oneTimeKeys().value("curve25519").toObject().toVariantHash(); } QVariantHash Account::ed25519OneTimeKeys() { return oneTimeKeys().value("ed25519").toObject().toVariantHash(); } void Account::removeOneTimeKeys(Session* session) { checkErr(olm_remove_one_time_keys(_account, session->session())); } OlmAccount* Account::newAccount() { return olm_account(new uint8_t[olm_account_size()]); } void Account::checkErr(size_t code) { if (code != olm_error()) return; std::string lastError = olm_account_last_error(_account); if (lastError == "SUCCESS") return; if (lastError == "NOT_ENOUGH_RANDOM") throw new EntropyError(lastError); if (lastError == "OUTPUT_BUFFER_TOO_SMALL" || lastError == "OLM_INPUT_BUFFER_TOO_SMALL") throw new BufferError(lastError); if (lastError == "BAD_MESSAGE_VERSION" || lastError == "BAD_MESSAGE_FORMAT" || lastError == "BAD_MESSAGE_MAC" || lastError == "BAD_MESSAGE_KEY_ID" || lastError == "UNKNOWN_MESSAGE_INDEX") throw new MessageError(lastError); if (lastError == "INVALID_BASE64") throw new Base64Error(lastError); if (lastError == "BAD_ACCOUNT_KEY") throw new AccountKeyError(lastError); if (lastError == "UNKNOWN_PICKLE_VERSION" || lastError == "CORRUPTED_PICKLE" || lastError == "BAD_LEGACY_ACCOUNT_PICKLE") throw new PickleError(lastError); if (lastError == "BAD_SESSION_KEY") throw new SessionKeyError(lastError); if (lastError == "BAD_SIGNATURE") throw new SignatureError(lastError); throw new OlmError(lastError); } spectral/include/libQuotient/3rdparty/libQtOlm/lib/account.h0000644000175000000620000000200313566674122024147 0ustar dilingerstaff#ifndef ACCOUNT_H #define ACCOUNT_H #include "olm/olm.h" #include namespace QtOlm { class Session; class Account : public QObject { Q_OBJECT public: explicit Account(QObject* parent = nullptr); Account(QByteArray pickle, QString passphrase = "", QObject* parent = nullptr); ~Account(); QByteArray pickle(QString passphrase = ""); QPair identityKeys(); QByteArray curve25519IdentityKey(); QByteArray ed25519IdentityKey(); QByteArray sign(QString message); QByteArray sign(QJsonObject message); int maxOneTimeKeys(); void markKeysAsPublished(); void generateOneTimeKeys(int count); QJsonObject oneTimeKeys(); QVariantHash curve25519OneTimeKeys(); QVariantHash ed25519OneTimeKeys(); void removeOneTimeKeys(Session* session); OlmAccount* account() { return _account; } private: static OlmAccount* newAccount(); protected: OlmAccount* _account; void checkErr(size_t code); }; } // namespace QtOlm #endif // ACCOUNT_H spectral/include/libQuotient/3rdparty/libQtOlm/lib/pk.cpp0000644000175000000620000001464113566674122023473 0ustar dilingerstaff#include "pk.h" #include "utils.h" using namespace QtOlm; PkMessage::PkMessage(QByteArray ephermalKey, QByteArray mac, QByteArray cipherText, QObject* parent) : QObject(parent), _ephermalKey(ephermalKey), _mac(mac), _cipherText(cipherText) {} PkEncryption::PkEncryption(QByteArray recipientKey, QObject* parent) : QObject(parent), _pkEncryption(newPkEncryption()) { if (recipientKey.isEmpty()) throw new InvalidArgument("Recipient key is empty"); olm_pk_encryption_set_recipient_key(_pkEncryption, recipientKey, recipientKey.length()); } PkEncryption::~PkEncryption() { olm_clear_pk_encryption(_pkEncryption); } PkMessage* PkEncryption::encrypt(QString plainText) { std::string plainTxt = plainText.toStdString(); size_t encryptLength = olm_pk_encrypt_random_length(_pkEncryption); QByteArray randomBuffer = getRandomData(encryptLength); size_t cipherTextLength = olm_pk_ciphertext_length(_pkEncryption, plainTxt.length()); QByteArray cipherText(cipherTextLength, '0'); size_t macLength = olm_pk_mac_length(_pkEncryption); QByteArray mac(macLength, '0'); size_t ephermalKeySize = olm_pk_key_length(); QByteArray ephermalKey(ephermalKeySize, '0'); checkErr(olm_pk_encrypt(_pkEncryption, plainTxt.data(), plainTxt.length(), cipherText.data(), cipherTextLength, mac.data(), macLength, ephermalKey.data(), ephermalKeySize, randomBuffer.data(), encryptLength)); return new PkMessage(ephermalKey, mac, cipherText); } OlmPkEncryption* PkEncryption::newPkEncryption() { return olm_pk_encryption(new uint8_t[olm_pk_encryption_size()]); } void PkEncryption::checkErr(size_t code) { if (code != olm_error()) return; std::string lastError = olm_pk_encryption_last_error(_pkEncryption); if (lastError == "SUCCESS") return; if (lastError == "NOT_ENOUGH_RANDOM") throw new EntropyError(lastError); if (lastError == "OUTPUT_BUFFER_TOO_SMALL" || lastError == "OLM_INPUT_BUFFER_TOO_SMALL") throw new BufferError(lastError); if (lastError == "BAD_MESSAGE_VERSION" || lastError == "BAD_MESSAGE_FORMAT" || lastError == "BAD_MESSAGE_MAC" || lastError == "BAD_MESSAGE_KEY_ID" || lastError == "UNKNOWN_MESSAGE_INDEX") throw new MessageError(lastError); if (lastError == "INVALID_BASE64") throw new Base64Error(lastError); if (lastError == "BAD_ACCOUNT_KEY") throw new AccountKeyError(lastError); if (lastError == "UNKNOWN_PICKLE_VERSION" || lastError == "CORRUPTED_PICKLE" || lastError == "BAD_LEGACY_ACCOUNT_PICKLE") throw new PickleError(lastError); if (lastError == "BAD_SESSION_KEY") throw new SessionKeyError(lastError); if (lastError == "BAD_SIGNATURE") throw new SignatureError(lastError); throw new OlmError(lastError); } PkDecryption::PkDecryption(QObject* parent) : QObject(parent), _pkDecryption(newPkDecryption()) { size_t randomLength = olm_pk_generate_key_random_length(); QByteArray randomBuffer = getRandomData(randomLength); size_t keyLength = olm_pk_key_length(); QByteArray keyBuffer(keyLength, '0'); checkErr(olm_pk_generate_key(_pkDecryption, keyBuffer.data(), keyLength, randomBuffer.data(), randomLength)); _publicKey = keyBuffer; } PkDecryption::PkDecryption(QByteArray pickle, QString passphrase, QObject* parent) : QObject(parent), _pkDecryption(newPkDecryption()) { if (pickle.isEmpty()) throw new InvalidArgument("Pickle is empty"); std::string pass = passphrase.toStdString(); size_t pubKeyLength = olm_pk_key_length(); QByteArray pubKeyBuffer(pubKeyLength, '0'); checkErr(olm_unpickle_pk_decryption(_pkDecryption, pass.data(), pass.length(), pickle.data(), pickle.length(), pubKeyBuffer.data(), pubKeyLength)); _publicKey = pubKeyBuffer; } PkDecryption::~PkDecryption() { olm_clear_pk_decryption(_pkDecryption); } QByteArray PkDecryption::pickle(QString passphrase) { std::string pass = passphrase.toStdString(); size_t pickleLength = olm_pickle_pk_decryption_length(_pkDecryption); QByteArray pickleBuffer(pickleLength, '0'); checkErr(olm_pickle_pk_decryption(_pkDecryption, pass.data(), pass.length(), pickleBuffer.data(), pickleLength)); return pickleBuffer; } QString PkDecryption::decrypt(PkMessage* message) { QByteArray ephermalKey = message->ephermalKey(); QByteArray mac = message->mac(); QByteArray cipherText = message->cipherText(); size_t maxPlaintextLength = olm_pk_max_plaintext_length(_pkDecryption, cipherText.length()); QByteArray plainTextBuffer(maxPlaintextLength, '0'); size_t plainTextLength = olm_pk_decrypt( _pkDecryption, ephermalKey.data(), ephermalKey.length(), mac.data(), mac.length(), cipherText.data(), cipherText.length(), plainTextBuffer.data(), maxPlaintextLength); checkErr(plainTextLength); plainTextBuffer.truncate(plainTextLength); return QString(plainTextBuffer); } OlmPkDecryption* PkDecryption::newPkDecryption() { return olm_pk_decryption(new uint8_t[olm_pk_decryption_size()]); } void PkDecryption::checkErr(size_t code) { if (code != olm_error()) return; std::string lastError = olm_pk_decryption_last_error(_pkDecryption); if (lastError == "SUCCESS") return; if (lastError == "NOT_ENOUGH_RANDOM") throw new EntropyError(lastError); if (lastError == "OUTPUT_BUFFER_TOO_SMALL" || lastError == "OLM_INPUT_BUFFER_TOO_SMALL") throw new BufferError(lastError); if (lastError == "BAD_MESSAGE_VERSION" || lastError == "BAD_MESSAGE_FORMAT" || lastError == "BAD_MESSAGE_MAC" || lastError == "BAD_MESSAGE_KEY_ID" || lastError == "UNKNOWN_MESSAGE_INDEX") throw new MessageError(lastError); if (lastError == "INVALID_BASE64") throw new Base64Error(lastError); if (lastError == "BAD_ACCOUNT_KEY") throw new AccountKeyError(lastError); if (lastError == "UNKNOWN_PICKLE_VERSION" || lastError == "CORRUPTED_PICKLE" || lastError == "BAD_LEGACY_ACCOUNT_PICKLE") throw new PickleError(lastError); if (lastError == "BAD_SESSION_KEY") throw new SessionKeyError(lastError); if (lastError == "BAD_SIGNATURE") throw new SignatureError(lastError); throw new OlmError(lastError); } spectral/include/libQuotient/3rdparty/libQtOlm/lib/message.cpp0000644000175000000620000000043013566674122024474 0ustar dilingerstaff#include "message.h" #include "errors.h" using namespace QtOlm; _Message::_Message(QByteArray cipher, size_t type, QObject* parent) : QObject(parent), _messageType(type), _cipherText(cipher) { if (cipher.isEmpty()) throw new InvalidArgument("Ciphertext is empty"); } spectral/include/libQuotient/3rdparty/libQtOlm/lib/session.h0000644000175000000620000000247213566674122024210 0ustar dilingerstaff#ifndef SESSION_H #define SESSION_H #include "account.h" #include "message.h" #include "olm/olm.h" #include namespace QtOlm { class Session : public QObject { Q_OBJECT Q_PROPERTY(QString id READ id) public: explicit Session(QObject* parent = nullptr); Session(QByteArray pickle, QString passphrase = "", QObject* parent = nullptr); ~Session(); QByteArray pickle(QString passphrase = ""); _Message* encrypt(QString plainText); QString decrypt(_Message* message); bool matches(PreKeyMessage* message, QString identityKey = ""); QString id(); OlmSession* session() { return _session; } private: static OlmSession* newSession(); protected: void checkErr(size_t code); OlmSession* _session; }; class InboundSession : public Session { Q_OBJECT public: explicit InboundSession(Account* account, PreKeyMessage* message, QByteArray identityKey = "", QObject* parent = nullptr); }; class OutboundSession : public Session { Q_OBJECT public: explicit OutboundSession(Account* account, QByteArray identityKey, QByteArray oneTimeKey, QObject* parent = nullptr); }; } // namespace QtOlm #endif // SESSION_H spectral/include/libQuotient/3rdparty/libQtOlm/lib/utils.h0000644000175000000620000000276213566674122023667 0ustar dilingerstaff#ifndef UTILS_H #define UTILS_H #include "olm/olm.h" #include #include #include #include #include #include #include #include #include #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) #include #else #ifndef QT_NO_QT_INCLUDE_WARN #pragma message "WARNING: You're using Qt<5.10. Using fallback options instead of QRandomGenerator." #pragma message "WARNING: It's highly recommended to update your Qt package!" #endif // QT_NO_QT_INCLUDE_WARN #endif // Qt 5.10 #include #include "errors.h" namespace QtOlm { static OlmUtility* utility = olm_utility(new uint8_t[olm_utility_size()]); static QByteArray getRandomData(int buffer_size) { QByteArray buffer(buffer_size, '0'); #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)) std::generate(buffer.begin(), buffer.end(), *QRandomGenerator::system()); #else std::generate(buffer.begin(), buffer.end(), std::rand); #endif // Qt 5.10 return buffer; } static void ed25519Verify(QByteArray key, QString message, QByteArray signature) { std::string msg = message.toStdString(); if (olm_ed25519_verify(utility, key.data(), key.length(), msg.data(), msg.length(), signature.data(), signature.length()) == olm_error()) throw new SignatureError("Signature is invalid"); } } // namespace QtOlm #endif // UTILS_H spectral/include/libQuotient/3rdparty/libQtOlm/LICENSE0000644000175000000620000010437113566674122022614 0ustar dilingerstaff GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. libQtOlm Copyright (C) 2018 Black Hat This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: libQtOlm Copyright (C) 2018 Black Hat This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . spectral/include/libQuotient/3rdparty/libQtOlm/.gitmodules0000644000175000000620000000000013566674122023744 0ustar dilingerstaffspectral/include/libQuotient/COPYING0000644000175000000620000006350213566674122017347 0ustar dilingerstaff GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! spectral/include/libQuotient/.gitmodules0000644000175000000620000000014413566674122020462 0ustar dilingerstaff[submodule "3rdparty/libQtOlm"] path = 3rdparty/libQtOlm url = https://gitlab.com/b0/libqtolm.git spectral/include/libQuotient/.travis.yml0000644000175000000620000000462013566674122020421 0ustar dilingerstafflanguage: cpp dist: bionic git: depth: false before_cache: - brew cleanup cache: directories: - $HOME/Library/Caches/Homebrew addons: apt: packages: - ninja-build - qt5-default - qtmultimedia5-dev - valgrind matrix: include: - os: linux compiler: gcc - os: linux compiler: clang - os: osx osx_image: xcode10.1 env: [ 'PATH=/usr/local/opt/qt/bin:$PATH' ] addons: homebrew: update: true packages: - qt5 before_install: - eval "${ENV_EVAL}" - if [ "$TRAVIS_OS_NAME" = "linux" ]; then USE_NINJA="-GNinja"; fi - if [ "$TRAVIS_OS_NAME" = "linux" ]; then VALGRIND="valgrind $VALGRIND_OPTIONS"; fi install: - git clone https://gitlab.matrix.org/matrix-org/olm.git - pushd olm - cmake . -Bbuild -DBUILD_SHARED_LIBS=NO -DCMAKE_INSTALL_PREFIX=install - cmake --build build - cmake --build build --target install - popd - git clone https://github.com/quotient-im/matrix-doc.git - git clone --recursive https://github.com/KitsuneRal/gtad.git - pushd gtad - cmake $USE_NINJA -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} . - cmake --build . - popd before_script: - mkdir build && pushd build - cmake $USE_NINJA -DMATRIX_DOC_PATH="matrix-doc" -DGTAD_PATH="gtad/gtad" -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_PREFIX=../install -DOlm_DIR=../olm/install/lib/cmake/Olm .. - cmake --build . --target update-api - popd script: - cmake --build build --target all - cmake --build build --target install # Build qmc-example with the installed library - mkdir build-example && pushd build-example - cmake -DCMAKE_PREFIX_PATH=../install -DOlm_DIR=../olm/install/lib/cmake/Olm ../examples - cmake --build . --target all - popd # Build with qmake - qmake qmc-example.pro "CONFIG += debug" "CONFIG -= app_bundle" "QMAKE_CC = $CC" "QMAKE_CXX = $CXX" "INCLUDEPATH += olm/include" "LIBS += -Lbuild/lib" "LIBS += -Lolm/install/lib" - make all # Run the qmake-compiled qmc-example under valgrind - if [ "$QMC_TEST_USER" != "" ]; then LD_LIBRARY_PATH="build/lib" $VALGRIND ./qmc-example "$QMC_TEST_USER" "$QMC_TEST_PWD" qmc-example-travis '#qmc-test:matrix.org' "Travis CI job $TRAVIS_JOB_NUMBER"; fi notifications: webhooks: urls: - "https://scalar.vector.im/api/neb/services/hooks/dHJhdmlzLWNpLyU0MGtpdHN1bmUlM0FtYXRyaXgub3JnLyUyMVBDelV0eHRPalV5U3hTZWxvZiUzQW1hdHJpeC5vcmc" on_success: change # always|never|change on_failure: always on_start: never spectral/linux/0002755000175000000620000000000013570506742013522 5ustar dilingerstaffspectral/linux/org.eu.encom.spectral.desktop0000644000175000000620000000034513566674120021231 0ustar dilingerstaff[Desktop Entry] Name=Spectral GenericName=Matrix Client Comment=IM client for the Matrix protocol Exec=spectral Terminal=false Icon=org.eu.encom.spectral Type=Application Categories=Network;InstantMessaging; Name[en_US]=Spectral

    This is the complete list of members for SortFilterProxyModel, including inherited members.