Merge branch 'develop' into 'master'

More UI Improvements

Closes #2 and #114

See merge request b0/spectral!46
This commit is contained in:
Black Hat 2019-05-01 04:02:42 +00:00
commit 23126c435e
66 changed files with 2281 additions and 2353 deletions

View File

@ -3,7 +3,7 @@ image: Visual Studio 2017
environment: environment:
DEPLOY_DIR: Spectral-%APPVEYOR_BUILD_VERSION% DEPLOY_DIR: Spectral-%APPVEYOR_BUILD_VERSION%
matrix: matrix:
- QTDIR: C:\Qt\5.12.1\msvc2017_64 - QTDIR: C:\Qt\5.12\msvc2017_64
VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat" VCVARS: "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Auxiliary\\Build\\vcvars64.bat"
PLATFORM: PLATFORM:

View File

@ -3,30 +3,42 @@ stages:
- deploy - deploy
build-flatpak: build-flatpak:
image: black0/flatpak image: registry.gitlab.com/b0/flatpak-kde-docker
stage: build stage: build
before_script: before_script:
- git submodule update --init --recursive - git submodule update --init --recursive
script: script:
- cd flatpak - cd flatpak
- flatpak-builder --force-clean --repo=repo build-dir org.eu.encom.spectral.yaml - flatpak-builder --force-clean --ccache --repo=repo build-dir org.eu.encom.spectral.yaml
- flatpak build-bundle repo spectral.flatpak org.eu.encom.spectral - flatpak build-bundle repo spectral.flatpak org.eu.encom.spectral
- cd ../ - cd ../
cache:
key: "flatpak-$CI_COMMIT_REF_SLUG"
paths:
- flatpak/.flatpak-builder
artifacts: artifacts:
paths: paths:
- flatpak/spectral.flatpak - flatpak/spectral.flatpak
build-appimage: build-appimage:
image: black0/qt image: registry.gitlab.com/b0/qt-docker
stage: build stage: build
before_script: before_script:
- git submodule update --init --recursive - git submodule update --init --recursive
script: script:
- mkdir -p ccache
- export CCACHE_BASEDIR=${CI_PROJECT_DIR}
- export CCACHE_DIR=${CI_PROJECT_DIR}/ccache
- /opt/qt512/bin/qt512-env.sh - /opt/qt512/bin/qt512-env.sh
- /opt/qt512/bin/qmake CONFIG+=debug CONFIG+=qml_debug PREFIX=/usr - /opt/qt512/bin/qmake CONFIG+=debug CONFIG+=qml_debug CONFIG+=ccache PREFIX=/usr
- make - make
- make INSTALL_ROOT=appdir install - make INSTALL_ROOT=appdir install
- /usr/bin/linuxdeployqt-continuous-x86_64.AppImage appdir/usr/share/applications/org.eu.encom.spectral.desktop -appimage -qmldir=qml -qmldir=imports -qmake=/opt/qt512/bin/qmake - /usr/bin/linuxdeployqt-continuous-x86_64.AppImage appdir/usr/share/applications/org.eu.encom.spectral.desktop -appimage -qmldir=qml -qmldir=imports -qmake=/opt/qt512/bin/qmake
cache:
key: "appimage-$CI_COMMIT_REF_SLUG"
paths:
- ccache/
artifacts: artifacts:
paths: paths:
- Spectral*.AppImage - Spectral*.AppImage

View File

@ -15,7 +15,7 @@ Spectral is a glossy cross-platform client for Matrix, the decentralized communi
There is a separate document for Spectral, including installing, compiling, etc. There is a separate document for Spectral, including installing, compiling, etc.
It is at [Spectral Doc](https://doc.spectral.encom.eu.org/) It is at [Spectral Doc](https://b0.gitlab.io/spectral-doc/)
## Contact ## Contact
@ -31,6 +31,10 @@ This program uses libqmatrixclient library and some C++ models from Quaternion.
[libqmatrixclient](https://github.com/QMatrixClient/libqmatrixclient) [libqmatrixclient](https://github.com/QMatrixClient/libqmatrixclient)
This program includes the source code of hoedown.
[Hoedown](https://github.com/hoedown/hoedown)
## Donation ## Donation
Donations are welcome! My Bitcoin wallet address is 1AmNvttxJ6zne8f2GEH8zMAMQuT4cMdnDN Donations are welcome! My Bitcoin wallet address is 1AmNvttxJ6zne8f2GEH8zMAMQuT4cMdnDN
@ -41,4 +45,4 @@ Donations are welcome! My Bitcoin wallet address is 1AmNvttxJ6zne8f2GEH8zMAMQuT4
This program is licensed under GNU General Public License, Version 3. This program is licensed under GNU General Public License, Version 3.
Exceptions are src/notifications/wintoastlib.c and wintoastlib.h, copied from https://github.com/mohabouje/WinToast and licensed under MIT. Exceptions are src/notifications/wintoastlib.c and wintoastlib.h, which are from https://github.com/mohabouje/WinToast and licensed under MIT.

Binary file not shown.

Binary file not shown.

View File

@ -1,219 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1280"
height="960"
viewBox="0 0 338.66666 254.00001"
version="1.1"
id="svg4636"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="roompanel-dark.svg">
<defs
id="defs4630" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="574.88953"
inkscape:cy="546.35799"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1050"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1" />
<metadata
id="metadata4633">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-42.999983)">
<circle
id="path4638"
cx="274.10834"
cy="259.42917"
r="75.40625"
style="opacity:0.25;fill:#13100e;fill-opacity:1;stroke-width:0.26458335" />
<circle
id="path4638-6"
cx="303.0802"
cy="169.86771"
r="48.286457"
style="opacity:0.25;fill:#13100e;fill-opacity:1;stroke-width:0.16942617" />
<circle
id="path4638-6-7"
cx="165.76144"
cy="82.290604"
r="25.135412"
style="opacity:0.25;fill:#13100e;fill-opacity:1;stroke-width:0.08819444" />
<circle
id="path4638-6-5"
cx="38.100006"
cy="195.3998"
r="76.332291"
style="opacity:0.25;fill:#13100e;fill-opacity:1;stroke-width:0.26783261" />
<circle
id="path4638-6-3"
cx="87.709373"
cy="148.70103"
r="41.01041"
style="opacity:0.25;fill:#13100e;fill-opacity:1;stroke-width:0.14389619" />
<circle
id="path4638-6-56"
cx="220.3979"
cy="100.94374"
r="48.286453"
style="opacity:0.25;fill:#13100e;fill-opacity:1;stroke-width:0.16942617" />
<g
id="g5310"
transform="matrix(0.18980272,0,0,0.18980272,163.39608,213.89968)"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero">
<path
id="path5231"
d="M 34.004,340.809 H 2 c -1.104,0 -2,-0.896 -2,-2 V 2 C 0,0.896 0.896,0 2,0 h 32.004 c 1.104,0 2,0.896 2,2 v 7.71 c 0,1.104 -0.896,2 -2,2 h -21.13 v 317.386 h 21.13 c 1.104,0 2,0.896 2,2.001 v 7.712 c 0,1.104 -0.896,2 -2,2 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5233"
d="m 10.875,9.711 v 321.386 h 23.13 v 7.711 H 1.999 V 2.001 h 32.006 v 7.71 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5235"
d="m 252.402,233.711 h -32.993 c -1.104,0 -2,-0.896 -2,-2 v -68.073 c 0,-3.949 -0.154,-7.722 -0.457,-11.213 -0.289,-3.282 -1.074,-6.153 -2.332,-8.53 -1.204,-2.276 -3.017,-4.119 -5.384,-5.476 -2.393,-1.362 -5.775,-2.056 -10.042,-2.056 -4.238,0 -7.674,0.798 -10.213,2.371 -2.565,1.596 -4.604,3.701 -6.053,6.258 -1.498,2.643 -2.51,5.694 -3.013,9.067 -0.526,3.513 -0.793,7.125 -0.793,10.741 v 66.91 c 0,1.104 -0.896,2 -2,2 h -32.991 c -1.104,0 -2,-0.896 -2,-2 v -67.373 c 0,-3.435 -0.078,-6.964 -0.228,-10.485 -0.148,-3.251 -0.767,-6.278 -1.841,-8.995 -1.018,-2.571 -2.667,-4.584 -5.047,-6.153 -2.372,-1.552 -6.029,-2.341 -10.865,-2.341 -1.372,0 -3.265,0.328 -5.629,0.976 -2.28,0.624 -4.536,1.826 -6.705,3.577 -2.152,1.732 -4.036,4.306 -5.605,7.655 -1.569,3.356 -2.367,7.877 -2.367,13.438 v 69.701 c 0,1.104 -0.895,2 -2,2 H 68.857 c -1.104,0 -2,-0.896 -2,-2 V 111.594 c 0,-1.104 0.896,-1.999 2,-1.999 h 31.13 c 1.104,0 2,0.896 2,1.999 v 11.007 c 3.834,-4.499 8.248,-8.152 13.173,-10.896 6.396,-3.559 13.799,-5.362 22.002,-5.362 7.846,0 15.127,1.548 21.642,4.604 5.794,2.722 10.424,7.26 13.791,13.52 3.449,-4.362 7.833,-8.306 13.071,-11.752 6.422,-4.228 14.102,-6.371 22.824,-6.371 6.499,0 12.625,0.807 18.209,2.399 5.686,1.628 10.635,4.271 14.712,7.857 4.088,3.605 7.318,8.357 9.601,14.123 2.25,5.719 3.391,12.649 3.391,20.604 v 80.384 c -0.001,1.104 -0.896,2 -2.001,2 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5237"
d="m 99.988,111.595 v 16.264 h 0.463 c 4.338,-6.191 9.563,-10.998 15.684,-14.406 6.117,-3.402 13.129,-5.11 21.027,-5.11 7.588,0 14.521,1.475 20.793,4.415 6.274,2.945 11.038,8.131 14.291,15.567 3.56,-5.265 8.4,-9.913 14.521,-13.94 6.117,-4.025 13.358,-6.042 21.724,-6.042 6.351,0 12.234,0.776 17.66,2.325 5.418,1.549 10.065,4.027 13.938,7.434 3.869,3.41 6.889,7.863 9.062,13.357 2.167,5.504 3.253,12.122 3.253,19.869 v 80.385 H 219.41 v -68.074 c 0,-4.025 -0.154,-7.82 -0.465,-11.385 -0.313,-3.56 -1.161,-6.656 -2.555,-9.293 -1.395,-2.631 -3.45,-4.724 -6.157,-6.274 -2.711,-1.543 -6.391,-2.322 -11.037,-2.322 -4.646,0 -8.403,0.896 -11.269,2.671 -2.868,1.784 -5.112,4.109 -6.737,6.971 -1.626,2.869 -2.711,6.12 -3.252,9.762 -0.545,3.638 -0.814,7.318 -0.814,11.035 v 66.91 h -32.991 v -67.375 c 0,-3.562 -0.081,-7.087 -0.23,-10.57 -0.158,-3.487 -0.814,-6.7 -1.978,-9.645 -1.162,-2.94 -3.099,-5.304 -5.809,-7.088 -2.711,-1.775 -6.699,-2.671 -11.965,-2.671 -1.551,0 -3.603,0.349 -6.156,1.048 -2.556,0.697 -5.036,2.016 -7.435,3.949 -2.404,1.938 -4.454,4.726 -6.158,8.363 -1.705,3.642 -2.556,8.402 -2.556,14.287 v 69.701 H 68.856 V 111.595 Z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5239"
d="m 304.909,236.733 c -5.883,0 -11.46,-0.729 -16.574,-2.163 -5.192,-1.464 -9.806,-3.774 -13.713,-6.871 -3.944,-3.117 -7.068,-7.111 -9.282,-11.871 -2.205,-4.733 -3.324,-10.412 -3.324,-16.876 0,-7.13 1.293,-13.117 3.846,-17.797 2.542,-4.674 5.877,-8.464 9.912,-11.263 3.97,-2.752 8.556,-4.842 13.63,-6.209 4.901,-1.322 9.937,-2.394 14.961,-3.184 4.986,-0.775 9.949,-1.404 14.754,-1.872 4.679,-0.452 8.88,-1.139 12.489,-2.039 3.412,-0.854 6.118,-2.09 8.042,-3.672 1.666,-1.37 2.416,-3.384 2.292,-6.151 -0.002,-3.289 -0.502,-5.816 -1.492,-7.595 -0.998,-1.798 -2.283,-3.15 -3.927,-4.138 -1.703,-1.02 -3.725,-1.713 -6.012,-2.062 -2.47,-0.37 -5.146,-0.557 -7.947,-0.557 -6.034,0 -10.789,1.271 -14.135,3.783 -3.233,2.424 -5.155,6.64 -5.714,12.527 -0.098,1.026 -0.961,1.812 -1.992,1.812 h -32.992 c -0.552,0 -1.079,-0.229 -1.457,-0.629 -0.376,-0.402 -0.572,-0.941 -0.54,-1.491 0.485,-8.073 2.55,-14.894 6.142,-20.272 3.548,-5.331 8.147,-9.682 13.661,-12.931 5.424,-3.191 11.612,-5.498 18.392,-6.857 6.684,-1.335 13.5,-2.013 20.26,-2.013 6.096,0 12.365,0.437 18.626,1.296 6.377,0.88 12.285,2.622 17.562,5.177 5.376,2.604 9.845,6.29 13.282,10.951 3.498,4.744 5.271,11.048 5.271,18.731 v 62.494 c 0,5.307 0.306,10.462 0.915,15.319 0.576,4.64 1.572,8.116 2.963,10.338 0.385,0.616 0.407,1.395 0.055,2.031 -0.353,0.635 -1.022,1.03 -1.75,1.03 h -33.457 c -0.861,0 -1.624,-0.55 -1.898,-1.367 -0.646,-1.941 -1.176,-3.939 -1.572,-5.936 -0.141,-0.696 -0.267,-1.402 -0.38,-2.12 -4.825,4.184 -10.349,7.24 -16.474,9.105 -7.299,2.218 -14.843,3.342 -22.423,3.342 z m 37.032,-60.072 c -0.809,0.409 -1.676,0.768 -2.596,1.074 -2.161,0.72 -4.511,1.326 -6.988,1.807 -2.442,0.475 -5.033,0.872 -7.699,1.186 -2.631,0.311 -5.251,0.697 -7.784,1.146 -2.329,0.433 -4.705,1.035 -7.051,1.792 -2.194,0.711 -4.114,1.667 -5.699,2.842 -1.531,1.128 -2.785,2.587 -3.731,4.335 -0.917,1.709 -1.385,3.97 -1.385,6.719 0,2.598 0.465,4.778 1.385,6.481 0.928,1.722 2.142,3.035 3.716,4.018 1.644,1.026 3.601,1.757 5.816,2.17 2.344,0.439 4.799,0.663 7.297,0.663 6.105,0 10.836,-0.996 14.063,-2.961 3.244,-1.973 5.666,-4.349 7.199,-7.062 1.568,-2.78 2.542,-5.62 2.892,-8.436 0.376,-3.019 0.565,-5.436 0.565,-7.187 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5241"
d="m 273.544,129.255 c 3.405,-5.113 7.744,-9.215 13.012,-12.316 5.264,-3.097 11.186,-5.303 17.771,-6.621 6.582,-1.315 13.205,-1.976 19.865,-1.976 6.042,0 12.158,0.428 18.354,1.277 6.195,0.855 11.85,2.522 16.962,4.997 5.111,2.477 9.292,5.926 12.546,10.338 3.253,4.414 4.879,10.262 4.879,17.543 v 62.494 c 0,5.428 0.31,10.611 0.931,15.567 0.615,4.959 1.701,8.676 3.251,11.153 H 347.66 c -0.621,-1.86 -1.126,-3.755 -1.511,-5.693 -0.39,-1.933 -0.661,-3.908 -0.813,-5.923 -5.267,5.422 -11.465,9.217 -18.585,11.386 -7.127,2.163 -14.407,3.251 -21.842,3.251 -5.733,0 -11.077,-0.698 -16.033,-2.09 -4.958,-1.395 -9.293,-3.562 -13.01,-6.51 -3.718,-2.938 -6.622,-6.656 -8.713,-11.147 -2.091,-4.491 -3.138,-9.84 -3.138,-16.033 0,-6.813 1.199,-12.43 3.604,-16.84 2.399,-4.417 5.495,-7.939 9.295,-10.575 3.793,-2.632 8.129,-4.607 13.01,-5.923 4.878,-1.315 9.795,-2.358 14.752,-3.137 4.957,-0.772 9.835,-1.393 14.638,-1.857 4.801,-0.466 9.062,-1.164 12.779,-2.093 3.718,-0.929 6.658,-2.282 8.829,-4.065 2.165,-1.781 3.172,-4.375 3.02,-7.785 0,-3.56 -0.58,-6.389 -1.742,-8.479 -1.161,-2.09 -2.711,-3.719 -4.646,-4.88 -1.937,-1.161 -4.183,-1.936 -6.737,-2.325 -2.557,-0.382 -5.309,-0.58 -8.248,-0.58 -6.506,0 -11.617,1.395 -15.335,4.183 -3.716,2.788 -5.889,7.437 -6.506,13.94 h -32.991 c 0.462,-7.742 2.395,-14.173 5.807,-19.281 z m 65.169,46.583 c -2.09,0.696 -4.337,1.275 -6.736,1.741 -2.402,0.465 -4.918,0.853 -7.551,1.161 -2.635,0.313 -5.268,0.698 -7.899,1.163 -2.48,0.461 -4.919,1.086 -7.317,1.857 -2.404,0.779 -4.495,1.822 -6.274,3.138 -1.784,1.317 -3.216,2.985 -4.3,4.994 -1.085,2.014 -1.626,4.571 -1.626,7.668 0,2.94 0.541,5.422 1.626,7.431 1.084,2.017 2.558,3.604 4.416,4.765 1.858,1.161 4.025,1.976 6.507,2.438 2.475,0.466 5.031,0.698 7.665,0.698 6.505,0 11.537,-1.082 15.103,-3.253 3.561,-2.166 6.192,-4.762 7.899,-7.785 1.702,-3.019 2.749,-6.072 3.137,-9.174 0.384,-3.097 0.58,-5.576 0.58,-7.434 V 172.93 c -1.396,1.243 -3.138,2.21 -5.23,2.908 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5243"
d="m 444.542,234.874 c -5.187,0 -10.173,-0.361 -14.823,-1.069 -4.802,-0.732 -9.104,-2.183 -12.779,-4.313 -3.789,-2.185 -6.821,-5.341 -9.006,-9.375 -2.163,-3.986 -3.26,-9.232 -3.26,-15.59 v -68.859 h -17.981 c -1.104,0 -2,-0.896 -2,-1.999 v -22.073 c 0,-1.104 0.896,-1.999 2,-1.999 h 17.981 V 75.582 c 0,-1.104 0.896,-2 2,-2 h 32.992 c 1.104,0 2,0.896 2,2 v 34.014 h 22.162 c 1.104,0 2,0.896 2,1.999 v 22.073 c 0,1.104 -0.896,1.999 -2,1.999 h -22.162 v 57.479 c 0,6.229 1.198,8.731 2.202,9.733 1.004,1.007 3.506,2.205 9.738,2.205 1.804,0 3.542,-0.076 5.161,-0.225 1.604,-0.144 3.174,-0.367 4.669,-0.665 0.13,-0.026 0.261,-0.039 0.391,-0.039 0.458,0 0.907,0.159 1.27,0.454 0.463,0.379 0.73,0.946 0.73,1.546 v 25.555 c 0,0.979 -0.707,1.813 -1.672,1.974 -2.834,0.472 -6.041,0.794 -9.527,0.957 -3.613,0.157 -6.91,0.233 -10.086,0.233 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5245"
d="m 463.825,111.595 v 22.072 h -24.161 v 59.479 c 0,5.573 0.928,9.292 2.788,11.149 1.856,1.859 5.576,2.788 11.152,2.788 1.859,0 3.638,-0.076 5.343,-0.232 1.703,-0.152 3.33,-0.388 4.878,-0.696 v 25.557 c -2.788,0.465 -5.887,0.773 -9.293,0.931 -3.407,0.149 -6.737,0.23 -9.99,0.23 -5.111,0 -9.953,-0.35 -14.521,-1.048 -4.571,-0.695 -8.597,-2.047 -12.081,-4.063 -3.486,-2.011 -6.236,-4.88 -8.248,-8.597 -2.016,-3.714 -3.021,-8.595 -3.021,-14.639 v -70.859 h -19.98 v -22.072 h 19.98 V 75.583 h 32.992 v 36.012 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5247"
d="m 512.613,233.711 h -32.991 c -1.104,0 -2,-0.896 -2,-2 V 111.594 c 0,-1.104 0.896,-1.999 2,-1.999 h 31.366 c 1.104,0 2,0.896 2,1.999 v 15.069 c 0.967,-1.516 2.034,-2.978 3.199,-4.382 2.754,-3.312 5.949,-6.182 9.496,-8.522 3.545,-2.332 7.385,-4.169 11.415,-5.462 4.056,-1.298 8.327,-1.954 12.691,-1.954 2.341,0 4.953,0.418 7.766,1.243 0.852,0.25 1.437,1.032 1.437,1.92 v 30.67 c 0,0.6 -0.269,1.167 -0.732,1.547 -0.361,0.296 -0.808,0.452 -1.265,0.452 -0.133,0 -0.265,-0.013 -0.398,-0.039 -1.484,-0.3 -3.299,-0.565 -5.392,-0.787 -2.098,-0.224 -4.136,-0.339 -6.062,-0.339 -5.706,0 -10.572,0.95 -14.467,2.823 -3.862,1.86 -7.012,4.428 -9.361,7.629 -2.389,3.263 -4.115,7.12 -5.127,11.47 -1.043,4.479 -1.574,9.409 -1.574,14.647 v 54.132 c -10e-4,1.104 -0.897,2 -2.001,2 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5249"
d="M 510.988,111.595 V 133.9 h 0.465 c 1.546,-3.72 3.636,-7.163 6.272,-10.341 2.634,-3.172 5.652,-5.885 9.06,-8.131 3.405,-2.242 7.047,-3.985 10.923,-5.228 3.868,-1.237 7.898,-1.859 12.081,-1.859 2.168,0 4.566,0.39 7.202,1.163 v 30.67 c -1.551,-0.312 -3.41,-0.584 -5.576,-0.814 -2.17,-0.233 -4.26,-0.35 -6.274,-0.35 -6.041,0 -11.152,1.01 -15.332,3.021 -4.182,2.014 -7.55,4.761 -10.107,8.247 -2.555,3.487 -4.379,7.55 -5.462,12.198 -1.083,4.645 -1.625,9.682 -1.625,15.102 v 54.133 H 479.624 V 111.595 Z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5251"
d="M 603.923,233.711 H 570.93 c -1.104,0 -2,-0.896 -2,-2 V 111.594 c 0,-1.104 0.896,-1.999 2,-1.999 h 32.994 c 1.104,0 2,0.896 2,1.999 v 120.117 c -10e-4,1.104 -0.897,2 -2.001,2 z m 0,-138.705 H 570.93 c -1.104,0 -2,-0.896 -2,-1.999 V 65.825 c 0,-1.104 0.896,-2 2,-2 h 32.994 c 1.104,0 2,0.896 2,2 v 27.182 c -10e-4,1.103 -0.897,1.999 -2.001,1.999 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5253"
d="M 570.93,93.007 V 65.824 h 32.994 v 27.183 z m 32.994,18.588 V 231.712 H 570.93 V 111.595 Z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5255"
d="m 742.163,233.711 h -37.64 c -0.671,0 -1.297,-0.335 -1.667,-0.896 l -23.426,-35.352 -23.426,35.352 c -0.369,0.561 -0.995,0.896 -1.667,0.896 h -36.938 c -0.741,0 -1.424,-0.411 -1.77,-1.067 -0.345,-0.654 -0.3,-1.449 0.118,-2.061 l 42.435,-62.055 -38.71,-55.793 c -0.424,-0.613 -0.474,-1.408 -0.128,-2.069 0.343,-0.658 1.028,-1.071 1.771,-1.071 h 37.636 c 0.665,0 1.287,0.33 1.658,0.882 l 19.477,28.893 19.255,-28.884 c 0.372,-0.556 0.996,-0.891 1.665,-0.891 h 36.475 c 0.746,0 1.43,0.415 1.776,1.078 0.343,0.66 0.289,1.46 -0.139,2.071 l -38.69,55.082 43.578,62.744 c 0.424,0.61 0.474,1.408 0.128,2.066 -0.343,0.662 -1.026,1.075 -1.771,1.075 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5257"
d="m 621.115,111.595 h 37.637 l 21.144,31.365 20.911,-31.365 h 36.476 l -39.496,56.226 44.377,63.892 h -37.64 l -25.093,-37.87 -25.094,37.87 h -36.938 l 43.213,-63.193 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5259"
d="m 791.322,340.809 h -32.008 c -1.105,0 -2,-0.896 -2,-2 v -7.712 c 0,-1.105 0.896,-2.001 2,-2.001 h 21.13 V 11.71 h -21.13 c -1.105,0 -2,-0.896 -2,-2 V 2 c 0,-1.104 0.896,-2 2,-2 h 32.008 c 1.104,0 2,0.896 2,2 v 336.809 c 0,1.104 -0.896,2 -2,2 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5261"
d="M 782.443,331.097 V 9.711 h -23.13 v -7.71 h 32.008 v 336.807 h -32.008 v -7.711 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5263"
d="m 10.875,9.711 v 321.386 h 23.13 v 7.711 H 1.999 V 2.001 h 32.006 v 7.71 z"
inkscape:connector-curvature="0"
style="fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5265"
d="m 99.988,111.595 v 16.264 h 0.463 c 4.338,-6.191 9.563,-10.998 15.684,-14.406 6.117,-3.402 13.129,-5.11 21.027,-5.11 7.588,0 14.521,1.475 20.793,4.415 6.274,2.945 11.038,8.131 14.291,15.567 3.56,-5.265 8.4,-9.913 14.521,-13.94 6.117,-4.025 13.358,-6.042 21.724,-6.042 6.351,0 12.234,0.776 17.66,2.325 5.418,1.549 10.065,4.027 13.938,7.434 3.869,3.41 6.889,7.863 9.062,13.357 2.167,5.504 3.253,12.122 3.253,19.869 v 80.385 H 219.41 v -68.074 c 0,-4.025 -0.154,-7.82 -0.465,-11.385 -0.313,-3.56 -1.161,-6.656 -2.555,-9.293 -1.395,-2.631 -3.45,-4.724 -6.157,-6.274 -2.711,-1.543 -6.391,-2.322 -11.037,-2.322 -4.646,0 -8.403,0.896 -11.269,2.671 -2.868,1.784 -5.112,4.109 -6.737,6.971 -1.626,2.869 -2.711,6.12 -3.252,9.762 -0.545,3.638 -0.814,7.318 -0.814,11.035 v 66.91 h -32.991 v -67.375 c 0,-3.562 -0.081,-7.087 -0.23,-10.57 -0.158,-3.487 -0.814,-6.7 -1.978,-9.645 -1.162,-2.94 -3.099,-5.304 -5.809,-7.088 -2.711,-1.775 -6.699,-2.671 -11.965,-2.671 -1.551,0 -3.603,0.349 -6.156,1.048 -2.556,0.697 -5.036,2.016 -7.435,3.949 -2.404,1.938 -4.454,4.726 -6.158,8.363 -1.705,3.642 -2.556,8.402 -2.556,14.287 v 69.701 H 68.856 V 111.595 Z"
inkscape:connector-curvature="0"
style="fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5267"
d="m 273.544,129.255 c 3.405,-5.113 7.744,-9.215 13.012,-12.316 5.264,-3.097 11.186,-5.303 17.771,-6.621 6.582,-1.315 13.205,-1.976 19.865,-1.976 6.042,0 12.158,0.428 18.354,1.277 6.195,0.855 11.85,2.522 16.962,4.997 5.111,2.477 9.292,5.926 12.546,10.338 3.253,4.414 4.879,10.262 4.879,17.543 v 62.494 c 0,5.428 0.31,10.611 0.931,15.567 0.615,4.959 1.701,8.676 3.251,11.153 H 347.66 c -0.621,-1.86 -1.126,-3.755 -1.511,-5.693 -0.39,-1.933 -0.661,-3.908 -0.813,-5.923 -5.267,5.422 -11.465,9.217 -18.585,11.386 -7.127,2.163 -14.407,3.251 -21.842,3.251 -5.733,0 -11.077,-0.698 -16.033,-2.09 -4.958,-1.395 -9.293,-3.562 -13.01,-6.51 -3.718,-2.938 -6.622,-6.656 -8.713,-11.147 -2.091,-4.491 -3.138,-9.84 -3.138,-16.033 0,-6.813 1.199,-12.43 3.604,-16.84 2.399,-4.417 5.495,-7.939 9.295,-10.575 3.793,-2.632 8.129,-4.607 13.01,-5.923 4.878,-1.315 9.795,-2.358 14.752,-3.137 4.957,-0.772 9.835,-1.393 14.638,-1.857 4.801,-0.466 9.062,-1.164 12.779,-2.093 3.718,-0.929 6.658,-2.282 8.829,-4.065 2.165,-1.781 3.172,-4.375 3.02,-7.785 0,-3.56 -0.58,-6.389 -1.742,-8.479 -1.161,-2.09 -2.711,-3.719 -4.646,-4.88 -1.937,-1.161 -4.183,-1.936 -6.737,-2.325 -2.557,-0.382 -5.309,-0.58 -8.248,-0.58 -6.506,0 -11.617,1.395 -15.335,4.183 -3.716,2.788 -5.889,7.437 -6.506,13.94 h -32.991 c 0.462,-7.742 2.395,-14.173 5.807,-19.281 z m 65.169,46.583 c -2.09,0.696 -4.337,1.275 -6.736,1.741 -2.402,0.465 -4.918,0.853 -7.551,1.161 -2.635,0.313 -5.268,0.698 -7.899,1.163 -2.48,0.461 -4.919,1.086 -7.317,1.857 -2.404,0.779 -4.495,1.822 -6.274,3.138 -1.784,1.317 -3.216,2.985 -4.3,4.994 -1.085,2.014 -1.626,4.571 -1.626,7.668 0,2.94 0.541,5.422 1.626,7.431 1.084,2.017 2.558,3.604 4.416,4.765 1.858,1.161 4.025,1.976 6.507,2.438 2.475,0.466 5.031,0.698 7.665,0.698 6.505,0 11.537,-1.082 15.103,-3.253 3.561,-2.166 6.192,-4.762 7.899,-7.785 1.702,-3.019 2.749,-6.072 3.137,-9.174 0.384,-3.097 0.58,-5.576 0.58,-7.434 V 172.93 c -1.396,1.243 -3.138,2.21 -5.23,2.908 z"
inkscape:connector-curvature="0"
style="fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5269"
d="m 463.825,111.595 v 22.072 h -24.161 v 59.479 c 0,5.573 0.928,9.292 2.788,11.149 1.856,1.859 5.576,2.788 11.152,2.788 1.859,0 3.638,-0.076 5.343,-0.232 1.703,-0.152 3.33,-0.388 4.878,-0.696 v 25.557 c -2.788,0.465 -5.887,0.773 -9.293,0.931 -3.407,0.149 -6.737,0.23 -9.99,0.23 -5.111,0 -9.953,-0.35 -14.521,-1.048 -4.571,-0.695 -8.597,-2.047 -12.081,-4.063 -3.486,-2.011 -6.236,-4.88 -8.248,-8.597 -2.016,-3.714 -3.021,-8.595 -3.021,-14.639 v -70.859 h -19.98 v -22.072 h 19.98 V 75.583 h 32.992 v 36.012 z"
inkscape:connector-curvature="0"
style="fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5271"
d="M 510.988,111.595 V 133.9 h 0.465 c 1.546,-3.72 3.636,-7.163 6.272,-10.341 2.634,-3.172 5.652,-5.885 9.06,-8.131 3.405,-2.242 7.047,-3.985 10.923,-5.228 3.868,-1.237 7.898,-1.859 12.081,-1.859 2.168,0 4.566,0.39 7.202,1.163 v 30.67 c -1.551,-0.312 -3.41,-0.584 -5.576,-0.814 -2.17,-0.233 -4.26,-0.35 -6.274,-0.35 -6.041,0 -11.152,1.01 -15.332,3.021 -4.182,2.014 -7.55,4.761 -10.107,8.247 -2.555,3.487 -4.379,7.55 -5.462,12.198 -1.083,4.645 -1.625,9.682 -1.625,15.102 v 54.133 H 479.624 V 111.595 Z"
inkscape:connector-curvature="0"
style="fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5273"
d="M 570.93,93.007 V 65.824 h 32.994 v 27.183 z m 32.994,18.588 V 231.712 H 570.93 V 111.595 Z"
inkscape:connector-curvature="0"
style="fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5275"
d="m 621.115,111.595 h 37.637 l 21.144,31.365 20.911,-31.365 h 36.476 l -39.496,56.226 44.377,63.892 h -37.64 l -25.093,-37.87 -25.094,37.87 h -36.938 l 43.213,-63.193 z"
inkscape:connector-curvature="0"
style="fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5277"
d="M 782.443,331.097 V 9.711 h -23.13 v -7.71 h 32.008 v 336.807 h -32.008 v -7.711 z"
inkscape:connector-curvature="0"
style="fill:#13100e;fill-opacity:1;fill-rule:nonzero" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,219 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1280"
height="960"
viewBox="0 0 338.66666 254.00001"
version="1.1"
id="svg4636"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="drawing.svg">
<defs
id="defs4630" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="686.38953"
inkscape:cy="426.35799"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1050"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1" />
<metadata
id="metadata4633">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-42.999983)">
<circle
id="path4638"
cx="274.10834"
cy="259.42917"
r="75.40625"
style="opacity:0.25;fill:#eceff1;fill-opacity:1;stroke-width:0.26458335" />
<circle
id="path4638-6"
cx="303.0802"
cy="169.86771"
r="48.286457"
style="opacity:0.25;fill:#eceff1;fill-opacity:1;stroke-width:0.16942617" />
<circle
id="path4638-6-7"
cx="165.76144"
cy="82.290604"
r="25.135412"
style="opacity:0.25;fill:#eceff1;fill-opacity:1;stroke-width:0.08819444" />
<circle
id="path4638-6-5"
cx="38.100006"
cy="195.3998"
r="76.332291"
style="opacity:0.25;fill:#eceff1;fill-opacity:1;stroke-width:0.26783261" />
<circle
id="path4638-6-3"
cx="87.709373"
cy="148.70103"
r="41.01041"
style="opacity:0.25;fill:#eceff1;fill-opacity:1;stroke-width:0.14389619" />
<circle
id="path4638-6-56"
cx="220.3979"
cy="100.94374"
r="48.286453"
style="opacity:0.25;fill:#eceff1;fill-opacity:1;stroke-width:0.16942617" />
<g
id="g5310"
transform="matrix(0.18980272,0,0,0.18980272,163.39608,213.89968)"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero">
<path
id="path5231"
d="M 34.004,340.809 H 2 c -1.104,0 -2,-0.896 -2,-2 V 2 C 0,0.896 0.896,0 2,0 h 32.004 c 1.104,0 2,0.896 2,2 v 7.71 c 0,1.104 -0.896,2 -2,2 h -21.13 v 317.386 h 21.13 c 1.104,0 2,0.896 2,2.001 v 7.712 c 0,1.104 -0.896,2 -2,2 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5233"
d="m 10.875,9.711 v 321.386 h 23.13 v 7.711 H 1.999 V 2.001 h 32.006 v 7.71 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5235"
d="m 252.402,233.711 h -32.993 c -1.104,0 -2,-0.896 -2,-2 v -68.073 c 0,-3.949 -0.154,-7.722 -0.457,-11.213 -0.289,-3.282 -1.074,-6.153 -2.332,-8.53 -1.204,-2.276 -3.017,-4.119 -5.384,-5.476 -2.393,-1.362 -5.775,-2.056 -10.042,-2.056 -4.238,0 -7.674,0.798 -10.213,2.371 -2.565,1.596 -4.604,3.701 -6.053,6.258 -1.498,2.643 -2.51,5.694 -3.013,9.067 -0.526,3.513 -0.793,7.125 -0.793,10.741 v 66.91 c 0,1.104 -0.896,2 -2,2 h -32.991 c -1.104,0 -2,-0.896 -2,-2 v -67.373 c 0,-3.435 -0.078,-6.964 -0.228,-10.485 -0.148,-3.251 -0.767,-6.278 -1.841,-8.995 -1.018,-2.571 -2.667,-4.584 -5.047,-6.153 -2.372,-1.552 -6.029,-2.341 -10.865,-2.341 -1.372,0 -3.265,0.328 -5.629,0.976 -2.28,0.624 -4.536,1.826 -6.705,3.577 -2.152,1.732 -4.036,4.306 -5.605,7.655 -1.569,3.356 -2.367,7.877 -2.367,13.438 v 69.701 c 0,1.104 -0.895,2 -2,2 H 68.857 c -1.104,0 -2,-0.896 -2,-2 V 111.594 c 0,-1.104 0.896,-1.999 2,-1.999 h 31.13 c 1.104,0 2,0.896 2,1.999 v 11.007 c 3.834,-4.499 8.248,-8.152 13.173,-10.896 6.396,-3.559 13.799,-5.362 22.002,-5.362 7.846,0 15.127,1.548 21.642,4.604 5.794,2.722 10.424,7.26 13.791,13.52 3.449,-4.362 7.833,-8.306 13.071,-11.752 6.422,-4.228 14.102,-6.371 22.824,-6.371 6.499,0 12.625,0.807 18.209,2.399 5.686,1.628 10.635,4.271 14.712,7.857 4.088,3.605 7.318,8.357 9.601,14.123 2.25,5.719 3.391,12.649 3.391,20.604 v 80.384 c -0.001,1.104 -0.896,2 -2.001,2 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5237"
d="m 99.988,111.595 v 16.264 h 0.463 c 4.338,-6.191 9.563,-10.998 15.684,-14.406 6.117,-3.402 13.129,-5.11 21.027,-5.11 7.588,0 14.521,1.475 20.793,4.415 6.274,2.945 11.038,8.131 14.291,15.567 3.56,-5.265 8.4,-9.913 14.521,-13.94 6.117,-4.025 13.358,-6.042 21.724,-6.042 6.351,0 12.234,0.776 17.66,2.325 5.418,1.549 10.065,4.027 13.938,7.434 3.869,3.41 6.889,7.863 9.062,13.357 2.167,5.504 3.253,12.122 3.253,19.869 v 80.385 H 219.41 v -68.074 c 0,-4.025 -0.154,-7.82 -0.465,-11.385 -0.313,-3.56 -1.161,-6.656 -2.555,-9.293 -1.395,-2.631 -3.45,-4.724 -6.157,-6.274 -2.711,-1.543 -6.391,-2.322 -11.037,-2.322 -4.646,0 -8.403,0.896 -11.269,2.671 -2.868,1.784 -5.112,4.109 -6.737,6.971 -1.626,2.869 -2.711,6.12 -3.252,9.762 -0.545,3.638 -0.814,7.318 -0.814,11.035 v 66.91 h -32.991 v -67.375 c 0,-3.562 -0.081,-7.087 -0.23,-10.57 -0.158,-3.487 -0.814,-6.7 -1.978,-9.645 -1.162,-2.94 -3.099,-5.304 -5.809,-7.088 -2.711,-1.775 -6.699,-2.671 -11.965,-2.671 -1.551,0 -3.603,0.349 -6.156,1.048 -2.556,0.697 -5.036,2.016 -7.435,3.949 -2.404,1.938 -4.454,4.726 -6.158,8.363 -1.705,3.642 -2.556,8.402 -2.556,14.287 v 69.701 H 68.856 V 111.595 Z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5239"
d="m 304.909,236.733 c -5.883,0 -11.46,-0.729 -16.574,-2.163 -5.192,-1.464 -9.806,-3.774 -13.713,-6.871 -3.944,-3.117 -7.068,-7.111 -9.282,-11.871 -2.205,-4.733 -3.324,-10.412 -3.324,-16.876 0,-7.13 1.293,-13.117 3.846,-17.797 2.542,-4.674 5.877,-8.464 9.912,-11.263 3.97,-2.752 8.556,-4.842 13.63,-6.209 4.901,-1.322 9.937,-2.394 14.961,-3.184 4.986,-0.775 9.949,-1.404 14.754,-1.872 4.679,-0.452 8.88,-1.139 12.489,-2.039 3.412,-0.854 6.118,-2.09 8.042,-3.672 1.666,-1.37 2.416,-3.384 2.292,-6.151 -0.002,-3.289 -0.502,-5.816 -1.492,-7.595 -0.998,-1.798 -2.283,-3.15 -3.927,-4.138 -1.703,-1.02 -3.725,-1.713 -6.012,-2.062 -2.47,-0.37 -5.146,-0.557 -7.947,-0.557 -6.034,0 -10.789,1.271 -14.135,3.783 -3.233,2.424 -5.155,6.64 -5.714,12.527 -0.098,1.026 -0.961,1.812 -1.992,1.812 h -32.992 c -0.552,0 -1.079,-0.229 -1.457,-0.629 -0.376,-0.402 -0.572,-0.941 -0.54,-1.491 0.485,-8.073 2.55,-14.894 6.142,-20.272 3.548,-5.331 8.147,-9.682 13.661,-12.931 5.424,-3.191 11.612,-5.498 18.392,-6.857 6.684,-1.335 13.5,-2.013 20.26,-2.013 6.096,0 12.365,0.437 18.626,1.296 6.377,0.88 12.285,2.622 17.562,5.177 5.376,2.604 9.845,6.29 13.282,10.951 3.498,4.744 5.271,11.048 5.271,18.731 v 62.494 c 0,5.307 0.306,10.462 0.915,15.319 0.576,4.64 1.572,8.116 2.963,10.338 0.385,0.616 0.407,1.395 0.055,2.031 -0.353,0.635 -1.022,1.03 -1.75,1.03 h -33.457 c -0.861,0 -1.624,-0.55 -1.898,-1.367 -0.646,-1.941 -1.176,-3.939 -1.572,-5.936 -0.141,-0.696 -0.267,-1.402 -0.38,-2.12 -4.825,4.184 -10.349,7.24 -16.474,9.105 -7.299,2.218 -14.843,3.342 -22.423,3.342 z m 37.032,-60.072 c -0.809,0.409 -1.676,0.768 -2.596,1.074 -2.161,0.72 -4.511,1.326 -6.988,1.807 -2.442,0.475 -5.033,0.872 -7.699,1.186 -2.631,0.311 -5.251,0.697 -7.784,1.146 -2.329,0.433 -4.705,1.035 -7.051,1.792 -2.194,0.711 -4.114,1.667 -5.699,2.842 -1.531,1.128 -2.785,2.587 -3.731,4.335 -0.917,1.709 -1.385,3.97 -1.385,6.719 0,2.598 0.465,4.778 1.385,6.481 0.928,1.722 2.142,3.035 3.716,4.018 1.644,1.026 3.601,1.757 5.816,2.17 2.344,0.439 4.799,0.663 7.297,0.663 6.105,0 10.836,-0.996 14.063,-2.961 3.244,-1.973 5.666,-4.349 7.199,-7.062 1.568,-2.78 2.542,-5.62 2.892,-8.436 0.376,-3.019 0.565,-5.436 0.565,-7.187 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5241"
d="m 273.544,129.255 c 3.405,-5.113 7.744,-9.215 13.012,-12.316 5.264,-3.097 11.186,-5.303 17.771,-6.621 6.582,-1.315 13.205,-1.976 19.865,-1.976 6.042,0 12.158,0.428 18.354,1.277 6.195,0.855 11.85,2.522 16.962,4.997 5.111,2.477 9.292,5.926 12.546,10.338 3.253,4.414 4.879,10.262 4.879,17.543 v 62.494 c 0,5.428 0.31,10.611 0.931,15.567 0.615,4.959 1.701,8.676 3.251,11.153 H 347.66 c -0.621,-1.86 -1.126,-3.755 -1.511,-5.693 -0.39,-1.933 -0.661,-3.908 -0.813,-5.923 -5.267,5.422 -11.465,9.217 -18.585,11.386 -7.127,2.163 -14.407,3.251 -21.842,3.251 -5.733,0 -11.077,-0.698 -16.033,-2.09 -4.958,-1.395 -9.293,-3.562 -13.01,-6.51 -3.718,-2.938 -6.622,-6.656 -8.713,-11.147 -2.091,-4.491 -3.138,-9.84 -3.138,-16.033 0,-6.813 1.199,-12.43 3.604,-16.84 2.399,-4.417 5.495,-7.939 9.295,-10.575 3.793,-2.632 8.129,-4.607 13.01,-5.923 4.878,-1.315 9.795,-2.358 14.752,-3.137 4.957,-0.772 9.835,-1.393 14.638,-1.857 4.801,-0.466 9.062,-1.164 12.779,-2.093 3.718,-0.929 6.658,-2.282 8.829,-4.065 2.165,-1.781 3.172,-4.375 3.02,-7.785 0,-3.56 -0.58,-6.389 -1.742,-8.479 -1.161,-2.09 -2.711,-3.719 -4.646,-4.88 -1.937,-1.161 -4.183,-1.936 -6.737,-2.325 -2.557,-0.382 -5.309,-0.58 -8.248,-0.58 -6.506,0 -11.617,1.395 -15.335,4.183 -3.716,2.788 -5.889,7.437 -6.506,13.94 h -32.991 c 0.462,-7.742 2.395,-14.173 5.807,-19.281 z m 65.169,46.583 c -2.09,0.696 -4.337,1.275 -6.736,1.741 -2.402,0.465 -4.918,0.853 -7.551,1.161 -2.635,0.313 -5.268,0.698 -7.899,1.163 -2.48,0.461 -4.919,1.086 -7.317,1.857 -2.404,0.779 -4.495,1.822 -6.274,3.138 -1.784,1.317 -3.216,2.985 -4.3,4.994 -1.085,2.014 -1.626,4.571 -1.626,7.668 0,2.94 0.541,5.422 1.626,7.431 1.084,2.017 2.558,3.604 4.416,4.765 1.858,1.161 4.025,1.976 6.507,2.438 2.475,0.466 5.031,0.698 7.665,0.698 6.505,0 11.537,-1.082 15.103,-3.253 3.561,-2.166 6.192,-4.762 7.899,-7.785 1.702,-3.019 2.749,-6.072 3.137,-9.174 0.384,-3.097 0.58,-5.576 0.58,-7.434 V 172.93 c -1.396,1.243 -3.138,2.21 -5.23,2.908 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5243"
d="m 444.542,234.874 c -5.187,0 -10.173,-0.361 -14.823,-1.069 -4.802,-0.732 -9.104,-2.183 -12.779,-4.313 -3.789,-2.185 -6.821,-5.341 -9.006,-9.375 -2.163,-3.986 -3.26,-9.232 -3.26,-15.59 v -68.859 h -17.981 c -1.104,0 -2,-0.896 -2,-1.999 v -22.073 c 0,-1.104 0.896,-1.999 2,-1.999 h 17.981 V 75.582 c 0,-1.104 0.896,-2 2,-2 h 32.992 c 1.104,0 2,0.896 2,2 v 34.014 h 22.162 c 1.104,0 2,0.896 2,1.999 v 22.073 c 0,1.104 -0.896,1.999 -2,1.999 h -22.162 v 57.479 c 0,6.229 1.198,8.731 2.202,9.733 1.004,1.007 3.506,2.205 9.738,2.205 1.804,0 3.542,-0.076 5.161,-0.225 1.604,-0.144 3.174,-0.367 4.669,-0.665 0.13,-0.026 0.261,-0.039 0.391,-0.039 0.458,0 0.907,0.159 1.27,0.454 0.463,0.379 0.73,0.946 0.73,1.546 v 25.555 c 0,0.979 -0.707,1.813 -1.672,1.974 -2.834,0.472 -6.041,0.794 -9.527,0.957 -3.613,0.157 -6.91,0.233 -10.086,0.233 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5245"
d="m 463.825,111.595 v 22.072 h -24.161 v 59.479 c 0,5.573 0.928,9.292 2.788,11.149 1.856,1.859 5.576,2.788 11.152,2.788 1.859,0 3.638,-0.076 5.343,-0.232 1.703,-0.152 3.33,-0.388 4.878,-0.696 v 25.557 c -2.788,0.465 -5.887,0.773 -9.293,0.931 -3.407,0.149 -6.737,0.23 -9.99,0.23 -5.111,0 -9.953,-0.35 -14.521,-1.048 -4.571,-0.695 -8.597,-2.047 -12.081,-4.063 -3.486,-2.011 -6.236,-4.88 -8.248,-8.597 -2.016,-3.714 -3.021,-8.595 -3.021,-14.639 v -70.859 h -19.98 v -22.072 h 19.98 V 75.583 h 32.992 v 36.012 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5247"
d="m 512.613,233.711 h -32.991 c -1.104,0 -2,-0.896 -2,-2 V 111.594 c 0,-1.104 0.896,-1.999 2,-1.999 h 31.366 c 1.104,0 2,0.896 2,1.999 v 15.069 c 0.967,-1.516 2.034,-2.978 3.199,-4.382 2.754,-3.312 5.949,-6.182 9.496,-8.522 3.545,-2.332 7.385,-4.169 11.415,-5.462 4.056,-1.298 8.327,-1.954 12.691,-1.954 2.341,0 4.953,0.418 7.766,1.243 0.852,0.25 1.437,1.032 1.437,1.92 v 30.67 c 0,0.6 -0.269,1.167 -0.732,1.547 -0.361,0.296 -0.808,0.452 -1.265,0.452 -0.133,0 -0.265,-0.013 -0.398,-0.039 -1.484,-0.3 -3.299,-0.565 -5.392,-0.787 -2.098,-0.224 -4.136,-0.339 -6.062,-0.339 -5.706,0 -10.572,0.95 -14.467,2.823 -3.862,1.86 -7.012,4.428 -9.361,7.629 -2.389,3.263 -4.115,7.12 -5.127,11.47 -1.043,4.479 -1.574,9.409 -1.574,14.647 v 54.132 c -10e-4,1.104 -0.897,2 -2.001,2 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5249"
d="M 510.988,111.595 V 133.9 h 0.465 c 1.546,-3.72 3.636,-7.163 6.272,-10.341 2.634,-3.172 5.652,-5.885 9.06,-8.131 3.405,-2.242 7.047,-3.985 10.923,-5.228 3.868,-1.237 7.898,-1.859 12.081,-1.859 2.168,0 4.566,0.39 7.202,1.163 v 30.67 c -1.551,-0.312 -3.41,-0.584 -5.576,-0.814 -2.17,-0.233 -4.26,-0.35 -6.274,-0.35 -6.041,0 -11.152,1.01 -15.332,3.021 -4.182,2.014 -7.55,4.761 -10.107,8.247 -2.555,3.487 -4.379,7.55 -5.462,12.198 -1.083,4.645 -1.625,9.682 -1.625,15.102 v 54.133 H 479.624 V 111.595 Z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5251"
d="M 603.923,233.711 H 570.93 c -1.104,0 -2,-0.896 -2,-2 V 111.594 c 0,-1.104 0.896,-1.999 2,-1.999 h 32.994 c 1.104,0 2,0.896 2,1.999 v 120.117 c -10e-4,1.104 -0.897,2 -2.001,2 z m 0,-138.705 H 570.93 c -1.104,0 -2,-0.896 -2,-1.999 V 65.825 c 0,-1.104 0.896,-2 2,-2 h 32.994 c 1.104,0 2,0.896 2,2 v 27.182 c -10e-4,1.103 -0.897,1.999 -2.001,1.999 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5253"
d="M 570.93,93.007 V 65.824 h 32.994 v 27.183 z m 32.994,18.588 V 231.712 H 570.93 V 111.595 Z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5255"
d="m 742.163,233.711 h -37.64 c -0.671,0 -1.297,-0.335 -1.667,-0.896 l -23.426,-35.352 -23.426,35.352 c -0.369,0.561 -0.995,0.896 -1.667,0.896 h -36.938 c -0.741,0 -1.424,-0.411 -1.77,-1.067 -0.345,-0.654 -0.3,-1.449 0.118,-2.061 l 42.435,-62.055 -38.71,-55.793 c -0.424,-0.613 -0.474,-1.408 -0.128,-2.069 0.343,-0.658 1.028,-1.071 1.771,-1.071 h 37.636 c 0.665,0 1.287,0.33 1.658,0.882 l 19.477,28.893 19.255,-28.884 c 0.372,-0.556 0.996,-0.891 1.665,-0.891 h 36.475 c 0.746,0 1.43,0.415 1.776,1.078 0.343,0.66 0.289,1.46 -0.139,2.071 l -38.69,55.082 43.578,62.744 c 0.424,0.61 0.474,1.408 0.128,2.066 -0.343,0.662 -1.026,1.075 -1.771,1.075 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5257"
d="m 621.115,111.595 h 37.637 l 21.144,31.365 20.911,-31.365 h 36.476 l -39.496,56.226 44.377,63.892 h -37.64 l -25.093,-37.87 -25.094,37.87 h -36.938 l 43.213,-63.193 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5259"
d="m 791.322,340.809 h -32.008 c -1.105,0 -2,-0.896 -2,-2 v -7.712 c 0,-1.105 0.896,-2.001 2,-2.001 h 21.13 V 11.71 h -21.13 c -1.105,0 -2,-0.896 -2,-2 V 2 c 0,-1.104 0.896,-2 2,-2 h 32.008 c 1.104,0 2,0.896 2,2 v 336.809 c 0,1.104 -0.896,2 -2,2 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5261"
d="M 782.443,331.097 V 9.711 h -23.13 v -7.71 h 32.008 v 336.807 h -32.008 v -7.711 z"
inkscape:connector-curvature="0"
style="opacity:0.5;fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5263"
d="m 10.875,9.711 v 321.386 h 23.13 v 7.711 H 1.999 V 2.001 h 32.006 v 7.71 z"
inkscape:connector-curvature="0"
style="fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5265"
d="m 99.988,111.595 v 16.264 h 0.463 c 4.338,-6.191 9.563,-10.998 15.684,-14.406 6.117,-3.402 13.129,-5.11 21.027,-5.11 7.588,0 14.521,1.475 20.793,4.415 6.274,2.945 11.038,8.131 14.291,15.567 3.56,-5.265 8.4,-9.913 14.521,-13.94 6.117,-4.025 13.358,-6.042 21.724,-6.042 6.351,0 12.234,0.776 17.66,2.325 5.418,1.549 10.065,4.027 13.938,7.434 3.869,3.41 6.889,7.863 9.062,13.357 2.167,5.504 3.253,12.122 3.253,19.869 v 80.385 H 219.41 v -68.074 c 0,-4.025 -0.154,-7.82 -0.465,-11.385 -0.313,-3.56 -1.161,-6.656 -2.555,-9.293 -1.395,-2.631 -3.45,-4.724 -6.157,-6.274 -2.711,-1.543 -6.391,-2.322 -11.037,-2.322 -4.646,0 -8.403,0.896 -11.269,2.671 -2.868,1.784 -5.112,4.109 -6.737,6.971 -1.626,2.869 -2.711,6.12 -3.252,9.762 -0.545,3.638 -0.814,7.318 -0.814,11.035 v 66.91 h -32.991 v -67.375 c 0,-3.562 -0.081,-7.087 -0.23,-10.57 -0.158,-3.487 -0.814,-6.7 -1.978,-9.645 -1.162,-2.94 -3.099,-5.304 -5.809,-7.088 -2.711,-1.775 -6.699,-2.671 -11.965,-2.671 -1.551,0 -3.603,0.349 -6.156,1.048 -2.556,0.697 -5.036,2.016 -7.435,3.949 -2.404,1.938 -4.454,4.726 -6.158,8.363 -1.705,3.642 -2.556,8.402 -2.556,14.287 v 69.701 H 68.856 V 111.595 Z"
inkscape:connector-curvature="0"
style="fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5267"
d="m 273.544,129.255 c 3.405,-5.113 7.744,-9.215 13.012,-12.316 5.264,-3.097 11.186,-5.303 17.771,-6.621 6.582,-1.315 13.205,-1.976 19.865,-1.976 6.042,0 12.158,0.428 18.354,1.277 6.195,0.855 11.85,2.522 16.962,4.997 5.111,2.477 9.292,5.926 12.546,10.338 3.253,4.414 4.879,10.262 4.879,17.543 v 62.494 c 0,5.428 0.31,10.611 0.931,15.567 0.615,4.959 1.701,8.676 3.251,11.153 H 347.66 c -0.621,-1.86 -1.126,-3.755 -1.511,-5.693 -0.39,-1.933 -0.661,-3.908 -0.813,-5.923 -5.267,5.422 -11.465,9.217 -18.585,11.386 -7.127,2.163 -14.407,3.251 -21.842,3.251 -5.733,0 -11.077,-0.698 -16.033,-2.09 -4.958,-1.395 -9.293,-3.562 -13.01,-6.51 -3.718,-2.938 -6.622,-6.656 -8.713,-11.147 -2.091,-4.491 -3.138,-9.84 -3.138,-16.033 0,-6.813 1.199,-12.43 3.604,-16.84 2.399,-4.417 5.495,-7.939 9.295,-10.575 3.793,-2.632 8.129,-4.607 13.01,-5.923 4.878,-1.315 9.795,-2.358 14.752,-3.137 4.957,-0.772 9.835,-1.393 14.638,-1.857 4.801,-0.466 9.062,-1.164 12.779,-2.093 3.718,-0.929 6.658,-2.282 8.829,-4.065 2.165,-1.781 3.172,-4.375 3.02,-7.785 0,-3.56 -0.58,-6.389 -1.742,-8.479 -1.161,-2.09 -2.711,-3.719 -4.646,-4.88 -1.937,-1.161 -4.183,-1.936 -6.737,-2.325 -2.557,-0.382 -5.309,-0.58 -8.248,-0.58 -6.506,0 -11.617,1.395 -15.335,4.183 -3.716,2.788 -5.889,7.437 -6.506,13.94 h -32.991 c 0.462,-7.742 2.395,-14.173 5.807,-19.281 z m 65.169,46.583 c -2.09,0.696 -4.337,1.275 -6.736,1.741 -2.402,0.465 -4.918,0.853 -7.551,1.161 -2.635,0.313 -5.268,0.698 -7.899,1.163 -2.48,0.461 -4.919,1.086 -7.317,1.857 -2.404,0.779 -4.495,1.822 -6.274,3.138 -1.784,1.317 -3.216,2.985 -4.3,4.994 -1.085,2.014 -1.626,4.571 -1.626,7.668 0,2.94 0.541,5.422 1.626,7.431 1.084,2.017 2.558,3.604 4.416,4.765 1.858,1.161 4.025,1.976 6.507,2.438 2.475,0.466 5.031,0.698 7.665,0.698 6.505,0 11.537,-1.082 15.103,-3.253 3.561,-2.166 6.192,-4.762 7.899,-7.785 1.702,-3.019 2.749,-6.072 3.137,-9.174 0.384,-3.097 0.58,-5.576 0.58,-7.434 V 172.93 c -1.396,1.243 -3.138,2.21 -5.23,2.908 z"
inkscape:connector-curvature="0"
style="fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5269"
d="m 463.825,111.595 v 22.072 h -24.161 v 59.479 c 0,5.573 0.928,9.292 2.788,11.149 1.856,1.859 5.576,2.788 11.152,2.788 1.859,0 3.638,-0.076 5.343,-0.232 1.703,-0.152 3.33,-0.388 4.878,-0.696 v 25.557 c -2.788,0.465 -5.887,0.773 -9.293,0.931 -3.407,0.149 -6.737,0.23 -9.99,0.23 -5.111,0 -9.953,-0.35 -14.521,-1.048 -4.571,-0.695 -8.597,-2.047 -12.081,-4.063 -3.486,-2.011 -6.236,-4.88 -8.248,-8.597 -2.016,-3.714 -3.021,-8.595 -3.021,-14.639 v -70.859 h -19.98 v -22.072 h 19.98 V 75.583 h 32.992 v 36.012 z"
inkscape:connector-curvature="0"
style="fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5271"
d="M 510.988,111.595 V 133.9 h 0.465 c 1.546,-3.72 3.636,-7.163 6.272,-10.341 2.634,-3.172 5.652,-5.885 9.06,-8.131 3.405,-2.242 7.047,-3.985 10.923,-5.228 3.868,-1.237 7.898,-1.859 12.081,-1.859 2.168,0 4.566,0.39 7.202,1.163 v 30.67 c -1.551,-0.312 -3.41,-0.584 -5.576,-0.814 -2.17,-0.233 -4.26,-0.35 -6.274,-0.35 -6.041,0 -11.152,1.01 -15.332,3.021 -4.182,2.014 -7.55,4.761 -10.107,8.247 -2.555,3.487 -4.379,7.55 -5.462,12.198 -1.083,4.645 -1.625,9.682 -1.625,15.102 v 54.133 H 479.624 V 111.595 Z"
inkscape:connector-curvature="0"
style="fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5273"
d="M 570.93,93.007 V 65.824 h 32.994 v 27.183 z m 32.994,18.588 V 231.712 H 570.93 V 111.595 Z"
inkscape:connector-curvature="0"
style="fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5275"
d="m 621.115,111.595 h 37.637 l 21.144,31.365 20.911,-31.365 h 36.476 l -39.496,56.226 44.377,63.892 h -37.64 l -25.093,-37.87 -25.094,37.87 h -36.938 l 43.213,-63.193 z"
inkscape:connector-curvature="0"
style="fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
<path
id="path5277"
d="M 782.443,331.097 V 9.711 h -23.13 v -7.71 h 32.008 v 336.807 h -32.008 v -7.711 z"
inkscape:connector-curvature="0"
style="fill:#eceff1;fill-opacity:1;fill-rule:nonzero" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -11,12 +11,9 @@ finish-args:
- --device=dri - --device=dri
- --filesystem=xdg-download - --filesystem=xdg-download
- --talk-name=org.freedesktop.Notifications - --talk-name=org.freedesktop.Notifications
- --talk-name=org.kde.StatusNotifierWatcher
modules: modules:
- name: spectral - name: spectral
buildsystem: qmake buildsystem: qmake
config-opts:
- "BUNDLE_FONT=true"
sources: sources:
- type: dir - type: dir
path: ../ path: ../

View File

@ -1,6 +0,0 @@
<RCC>
<qresource prefix="/">
<file>assets/font/roboto.ttf</file>
<file>assets/font/twemoji.ttf</file>
</qresource>
</RCC>

View File

@ -6,8 +6,8 @@ MouseArea {
signal primaryClicked() signal primaryClicked()
signal secondaryClicked() signal secondaryClicked()
acceptedButtons: MSettings.pressAndHold ? Qt.LeftButton : (Qt.LeftButton | Qt.RightButton) acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse.button == Qt.RightButton ? secondaryClicked() : primaryClicked() onClicked: mouse.button == Qt.RightButton ? secondaryClicked() : primaryClicked()
onPressAndHold: MSettings.pressAndHold ? secondaryClicked() : {} onPressAndHold: secondaryClicked()
} }

View File

@ -1,6 +1,62 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Controls.Material 2.3
TextField { TextField {
id: textField
selectByMouse: true 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
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 }
}
}
]
}
}
} }

View File

@ -36,8 +36,8 @@ Item {
visible: !realSource || image.status != Image.Ready visible: !realSource || image.status != Image.Ready
radius: height / 2 radius: height / 2
color: stringToColor(hint) color: stringToColor(hint)
antialiasing: true
Label { Label {
anchors.centerIn: parent anchors.centerIn: parent

View File

@ -0,0 +1,49 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
ApplicationWindow {
property string eventId
property url localPath
id: root
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
visible: true
visibility: Qt.WindowFullScreen
title: "Image View - " + eventId
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)
fillMode: Image.PreserveAspectFit
cache: false
source: localPath
}
ItemDelegate {
anchors.top: parent.top
anchors.right: parent.right
width: 64
height: 64
contentItem: MaterialIcon {
icon: "\ue5cd"
color: "white"
}
onClicked: root.destroy()
}
}

View File

@ -1,535 +0,0 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Quick Controls module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtQuick.Window 2.1
import Spectral.Setting 0.1
Item {
id: root
property int orientation: Qt.Horizontal
/*!
This property holds the delegate that will be instantiated between each
child item. Inside the delegate the following properties are available:
\table
\row \li readonly property bool styleData.index \li Specifies the index of the splitter handle. The handle
between the first and the second item will get index 0,
the next handle index 1 etc.
\row \li readonly property bool styleData.hovered \li The handle is being hovered.
\row \li readonly property bool styleData.pressed \li The handle is being pressed.
\row \li readonly property bool styleData.resizing \li The handle is being dragged.
\endtable
*/
property Component handleDelegate: Rectangle {
width: 1
height: 1
color: MSettings.darkTheme ? "#424242" : "#E1E1E1"
}
/*!
This propery is \c true when the user is resizing any of the items by
dragging on the splitter handles.
*/
property bool resizing: false
/*! \internal */
default property alias __contents: contents.data
/*! \internal */
property alias __items: splitterItems.children
/*! \internal */
property alias __handles: splitterHandles.children
clip: true
Component.onCompleted: d.init()
onWidthChanged: d.updateLayout()
onHeightChanged: d.updateLayout()
onOrientationChanged: d.changeOrientation()
/*! \qmlmethod void SplitView::addItem(Item item)
Add an item to the end of the view.
\since QtQuick.Controls 1.12 */
function addItem(item) {
d.updateLayoutGuard = true
d.addItem_impl(item)
d.calculateImplicitSize()
d.updateLayoutGuard = false
d.updateFillIndex()
}
/*! \qmlmethod void SplitView::removeItem(Item item)
Remove \a item from the view.
\since QtQuick.Controls 1.4 */
function removeItem(item) {
d.updateLayoutGuard = true
var result = d.removeItem_impl(item)
if (result !== null) {
d.calculateImplicitSize()
d.updateLayoutGuard = false
d.updateFillIndex()
}
else {
d.updateLayoutGuard = false
}
}
SystemPalette { id: pal }
QtObject {
id: d
readonly property string leftMargin: horizontal ? "leftMargin" : "topMargin"
readonly property string topMargin: horizontal ? "topMargin" : "leftMargin"
readonly property string rightMargin: horizontal ? "rightMargin" : "bottomMargin"
property bool horizontal: orientation == Qt.Horizontal
readonly property string minimum: horizontal ? "minimumWidth" : "minimumHeight"
readonly property string maximum: horizontal ? "maximumWidth" : "maximumHeight"
readonly property string otherMinimum: horizontal ? "minimumHeight" : "minimumWidth"
readonly property string otherMaximum: horizontal ? "maximumHeight" : "maximumWidth"
readonly property string offset: horizontal ? "x" : "y"
readonly property string otherOffset: horizontal ? "y" : "x"
readonly property string size: horizontal ? "width" : "height"
readonly property string otherSize: horizontal ? "height" : "width"
readonly property string implicitSize: horizontal ? "implicitWidth" : "implicitHeight"
readonly property string implicitOtherSize: horizontal ? "implicitHeight" : "implicitWidth"
property int fillIndex: -1
property bool updateLayoutGuard: true
function extraMarginSize(item, other) {
if (typeof(other) === 'undefined')
other = false;
if (other === horizontal)
// vertical
return item.Layout.topMargin + item.Layout.bottomMargin
return item.Layout.leftMargin + item.Layout.rightMargin
}
function addItem_impl(item)
{
// temporarily set fillIndex to new item
fillIndex = __items.length
if (splitterItems.children.length > 0)
handleLoader.createObject(splitterHandles, {"__handleIndex":splitterItems.children.length - 1})
item.parent = splitterItems
d.initItemConnections(item)
}
function initItemConnections(item)
{
// should match disconnections in terminateItemConnections
item.widthChanged.connect(d.updateLayout)
item.heightChanged.connect(d.updateLayout)
item.Layout.maximumWidthChanged.connect(d.updateLayout)
item.Layout.minimumWidthChanged.connect(d.updateLayout)
item.Layout.maximumHeightChanged.connect(d.updateLayout)
item.Layout.minimumHeightChanged.connect(d.updateLayout)
item.Layout.leftMarginChanged.connect(d.updateLayout)
item.Layout.topMarginChanged.connect(d.updateLayout)
item.Layout.rightMarginChanged.connect(d.updateLayout)
item.Layout.bottomMarginChanged.connect(d.updateLayout)
item.visibleChanged.connect(d.updateFillIndex)
item.Layout.fillWidthChanged.connect(d.updateFillIndex)
item.Layout.fillHeightChanged.connect(d.updateFillIndex)
}
function terminateItemConnections(item)
{
// should match connections in initItemConnections
item.widthChanged.disconnect(d.updateLayout)
item.heightChanged.disconnect(d.updateLayout)
item.Layout.maximumWidthChanged.disconnect(d.updateLayout)
item.Layout.minimumWidthChanged.disconnect(d.updateLayout)
item.Layout.maximumHeightChanged.disconnect(d.updateLayout)
item.Layout.minimumHeightChanged.disconnect(d.updateLayout)
item.visibleChanged.disconnect(d.updateFillIndex)
item.Layout.fillWidthChanged.disconnect(d.updateFillIndex)
item.Layout.fillHeightChanged.disconnect(d.updateFillIndex)
}
function removeItem_impl(item)
{
var pos = itemPos(item)
// Check pos range
if (pos < 0 || pos >= __items.length)
return null
// Temporary unset the fillIndex
fillIndex = __items.length - 1
// Remove the handle at the left/right of the item that
// is going to be removed
var handlePos = -1
var hasPrevious = pos > 0
var hasNext = (pos + 1) < __items.length
if (hasPrevious)
handlePos = pos-1
else if (hasNext)
handlePos = pos
if (handlePos >= 0) {
var handle = __handles[handlePos]
handle.visible = false
handle.parent = null
handle.destroy()
for (var i = handlePos; i < __handles.length; ++i)
__handles[i].__handleIndex = i
}
// Remove the item.
// Disconnect the item to be removed
terminateItemConnections(item)
item.parent = null
return item
}
function itemPos(item)
{
for (var i = 0; i < __items.length; ++i)
if (item === __items[i])
return i
return -1
}
function init()
{
for (var i=0; i<__contents.length; ++i) {
var item = __contents[i];
if (!item.hasOwnProperty("x"))
continue
addItem_impl(item)
i-- // item was removed from list
}
d.calculateImplicitSize()
d.updateLayoutGuard = false
d.updateFillIndex()
}
function updateFillIndex()
{
if (lastItem.visible !== root.visible)
return
var policy = (root.orientation === Qt.Horizontal) ? "fillWidth" : "fillHeight"
for (var i=0; i<__items.length-1; ++i) {
if (__items[i].Layout[policy] === true)
break;
}
d.fillIndex = i
d.updateLayout()
}
function changeOrientation()
{
if (__items.length == 0)
return;
d.updateLayoutGuard = true
// Swap width/height for items and handles:
for (var i=0; i<__items.length; ++i) {
var item = __items[i]
var tmp = item.x
item.x = item.y
item.y = tmp
tmp = item.width
item.width = item.height
item.height = tmp
var handle = __handles[i]
if (handle) {
tmp = handle.x
handle.x = handle.y
handle.y = handle.x
tmp = handle.width
handle.width = handle.height
handle.height = tmp
}
}
// Change d.horizontal explicit, since the binding will change too late:
d.horizontal = orientation == Qt.Horizontal
d.updateLayoutGuard = false
d.updateFillIndex()
}
function calculateImplicitSize()
{
var implicitSize = 0
var implicitOtherSize = 0
for (var i=0; i<__items.length; ++i) {
var item = __items[i];
implicitSize += clampedMinMax(item[d.size], item.Layout[minimum], item.Layout[maximum]) + extraMarginSize(item)
var os = clampedMinMax(item[otherSize], item.Layout[otherMinimum], item.Layout[otherMaximum]) + extraMarginSize(item, true)
implicitOtherSize = Math.max(implicitOtherSize, os)
var handle = __handles[i]
if (handle)
implicitSize += handle[d.size] //### Can handles have margins??
}
root[d.implicitSize] = implicitSize
root[d.implicitOtherSize] = implicitOtherSize
}
function clampedMinMax(value, minimum, maximum)
{
if (value < minimum)
value = minimum
if (value > maximum)
value = maximum
return value
}
function accumulatedSize(firstIndex, lastIndex, includeFillItemMinimum)
{
// Go through items and handles, and
// calculate their accummulated width.
var w = 0
for (var i=firstIndex; i<lastIndex; ++i) {
var item = __items[i]
if (item.visible || i == d.fillIndex) {
if (i !== d.fillIndex)
w += item[d.size] + extraMarginSize(item)
else if (includeFillItemMinimum && item.Layout[minimum] !== undefined)
w += item.Layout[minimum] + extraMarginSize(item)
}
var handle = __handles[i]
if (handle && handle.visible)
w += handle[d.size]
}
return w
}
function updateLayout()
{
// This function will reposition both handles and
// items according to the their width/height:
if (__items.length === 0)
return;
if (!lastItem.visible)
return;
if (d.updateLayoutGuard === true)
return
d.updateLayoutGuard = true
// Ensure all items within their min/max:
for (var i=0; i<__items.length; ++i) {
if (i !== d.fillIndex) {
var item = __items[i];
var clampedSize = clampedMinMax(item[d.size], item.Layout[d.minimum], item.Layout[d.maximum])
if (clampedSize != item[d.size])
item[d.size] = clampedSize
}
}
// Set size of fillItem to remaining available space.
// Special case: If SplitView size is zero, we leave fillItem with the size
// it already got, and assume that SplitView ends up with implicit size as size:
if (root[d.size] != 0) {
var fillItem = __items[fillIndex]
var superfluous = root[d.size] - d.accumulatedSize(0, __items.length, false)
fillItem[d.size] = clampedMinMax(superfluous - extraMarginSize(fillItem), fillItem.Layout[minimum], fillItem.Layout[maximum]);
}
// Position items and handles according to their width:
var lastVisibleItem, lastVisibleHandle, handle
var pos = 0;
for (i=0; i<__items.length; ++i) {
// Position item to the right of the previous visible handle:
item = __items[i];
if (item.visible || i == d.fillIndex) {
pos += item.Layout[leftMargin]
item[d.offset] = pos
item[d.otherOffset] = item.Layout[topMargin]
item[d.otherSize] = clampedMinMax(root[otherSize], item.Layout[otherMinimum], item.Layout[otherMaximum]) - extraMarginSize(item, true)
lastVisibleItem = item
pos += Math.max(0, item[d.size]) + item.Layout[rightMargin]
}
handle = __handles[i]
if (handle && handle.visible) {
handle[d.offset] = pos
handle[d.otherOffset] = 0 //### can handles have margins?
handle[d.otherSize] = root[d.otherSize]
lastVisibleHandle = handle
pos += handle[d.size]
}
}
d.updateLayoutGuard = false
}
}
Component {
id: handleLoader
Loader {
id: itemHandle
property int __handleIndex: -1
property QtObject styleData: QtObject {
readonly property int index: __handleIndex
readonly property alias hovered: mouseArea.containsMouse
readonly property alias pressed: mouseArea.pressed
readonly property bool resizing: mouseArea.drag.active
onResizingChanged: root.resizing = resizing
}
property bool resizeLeftItem: (d.fillIndex > __handleIndex)
visible: __items[__handleIndex + (resizeLeftItem ? 0 : 1)].visible
sourceComponent: handleDelegate
onWidthChanged: d.updateLayout()
onHeightChanged: d.updateLayout()
onXChanged: moveHandle()
onYChanged: moveHandle()
MouseArea {
id: mouseArea
anchors.fill: parent
property real defaultMargin: Screen.pixelDensity * 2
anchors.leftMargin: (parent.width <= 1) ? -defaultMargin : 0
anchors.rightMargin: (parent.width <= 1) ? -defaultMargin : 0
anchors.topMargin: (parent.height <= 1) ? -defaultMargin : 0
anchors.bottomMargin: (parent.height <= 1) ? -defaultMargin : 0
hoverEnabled: true
drag.threshold: 0
drag.target: parent
drag.axis: root.orientation === Qt.Horizontal ? Drag.XAxis : Drag.YAxis
cursorShape: root.orientation === Qt.Horizontal ? Qt.SplitHCursor : Qt.SplitVCursor
}
function moveHandle() {
// Moving the handle means resizing an item. Which one,
// left or right, depends on where the fillItem is.
// 'updateLayout' will be overridden in case new width violates max/min.
// 'updateLayout' will be triggered when an item changes width.
if (d.updateLayoutGuard)
return
var leftHandle, leftItem, rightItem, rightHandle
var leftEdge, rightEdge, newWidth, leftStopX, rightStopX
var i
if (resizeLeftItem) {
// Ensure that the handle is not crossing other handles. So
// find the first visible handle to the left to determine the left edge:
leftEdge = 0
for (i=__handleIndex-1; i>=0; --i) {
leftHandle = __handles[i]
if (leftHandle.visible) {
leftEdge = leftHandle[d.offset] + leftHandle[d.size]
break;
}
}
// Ensure: leftStopX >= itemHandle[d.offset] >= rightStopX
var min = d.accumulatedSize(__handleIndex+1, __items.length, true)
rightStopX = root[d.size] - min - itemHandle[d.size]
leftStopX = Math.max(leftEdge, itemHandle[d.offset])
itemHandle[d.offset] = Math.min(rightStopX, Math.max(leftStopX, itemHandle[d.offset]))
newWidth = itemHandle[d.offset] - leftEdge
leftItem = __items[__handleIndex]
// The next line will trigger 'updateLayout':
leftItem[d.size] = newWidth
} else {
// Resize item to the right.
// Ensure that the handle is not crossing other handles. So
// find the first visible handle to the right to determine the right edge:
rightEdge = root[d.size]
for (i=__handleIndex+1; i<__handles.length; ++i) {
rightHandle = __handles[i]
if (rightHandle.visible) {
rightEdge = rightHandle[d.offset]
break;
}
}
// Ensure: leftStopX <= itemHandle[d.offset] <= rightStopX
min = d.accumulatedSize(0, __handleIndex+1, true)
leftStopX = min - itemHandle[d.size]
rightStopX = Math.min((rightEdge - itemHandle[d.size]), itemHandle[d.offset])
itemHandle[d.offset] = Math.max(leftStopX, Math.min(itemHandle[d.offset], rightStopX))
newWidth = rightEdge - (itemHandle[d.offset] + itemHandle[d.size])
rightItem = __items[__handleIndex+1]
// The next line will trigger 'updateLayout':
rightItem[d.size] = newWidth
}
}
}
}
Item {
id: contents
visible: false
anchors.fill: parent
}
Item {
id: splitterItems
anchors.fill: parent
}
Item {
id: splitterHandles
anchors.fill: parent
}
Item {
id: lastItem
onVisibleChanged: d.updateFillIndex()
}
Component.onDestruction: {
for (var i=0; i<splitterItems.children.length; ++i) {
var item = splitterItems.children[i];
d.terminateItemConnections(item)
}
}
}

View File

@ -9,7 +9,10 @@ import Spectral 0.1
import Spectral.Setting 0.1 import Spectral.Setting 0.1
import Spectral.Component 2.0 import Spectral.Component 2.0
import Spectral.Dialog 2.0
import Spectral.Menu.Timeline 2.0
import Spectral.Font 0.1 import Spectral.Font 0.1
import Spectral.Effect 2.0
ColumnLayout { ColumnLayout {
readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote" || aboveEventType === "other") readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote" || aboveEventType === "other")
@ -52,6 +55,20 @@ ColumnLayout {
visible: avatarVisible visible: avatarVisible
hint: author.displayName hint: author.displayName
source: author.avatarMediaId source: author.avatarMediaId
Component {
id: userDetailDialog
UserDetailDialog {}
}
RippleEffect {
anchors.fill: parent
circular: true
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author}).open()
}
} }
Label { Label {
@ -94,7 +111,7 @@ ColumnLayout {
} }
Label { Label {
text: progressInfo.active ? (progressInfo.progress + "/" + progressInfo.total) : content.info.size text: progressInfo.active ? (progressInfo.progress + "/" + progressInfo.total) : content.info ? content.info.size : "Unknown"
color: MPalette.lighter color: MPalette.lighter
} }
} }
@ -109,52 +126,59 @@ ColumnLayout {
id: messageMouseArea id: messageMouseArea
onSecondaryClicked: messageContextMenu.popup() onSecondaryClicked: {
var contextMenu = fileDelegateContextMenu.createObject(ApplicationWindow.overlay)
contextMenu.viewSource.connect(function() {
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
})
contextMenu.downloadAndOpen.connect(downloadAndOpen)
contextMenu.saveFileAs.connect(saveFileAs)
contextMenu.reply.connect(function() {
roomPanelInput.replyUser = author
roomPanelInput.replyEventID = eventId
roomPanelInput.replyContent = message
roomPanelInput.isReply = true
roomPanelInput.focus()
})
contextMenu.redact.connect(function() {
currentRoom.redactEvent(eventId)
})
contextMenu.popup()
}
Menu { Component {
id: messageContextMenu id: messageSourceDialog
MenuItem { MessageSourceDialog {}
text: "View Source" }
onTriggered: { Component {
sourceDialog.sourceText = toolTip id: openFileDialog
sourceDialog.open()
}
}
MenuItem {
text: "Open Externally"
onTriggered: downloadAndOpen() OpenFileDialog {}
} }
MenuItem {
text: "Save As"
onTriggered: saveFileAs() Component {
} id: fileDelegateContextMenu
MenuItem {
text: "Reply"
onTriggered: { FileDelegateContextMenu {}
roomPanelInput.replyUser = author
roomPanelInput.replyEventID = eventId
roomPanelInput.replyContent = message
roomPanelInput.isReply = true
roomPanelInput.focus()
}
}
MenuItem {
text: "Redact"
onTriggered: currentRoom.redactEvent(eventId)
}
} }
} }
} }
} }
} }
function saveFileAs() { currentRoom.saveFileAs(eventId) } function saveFileAs() {
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay, {"selectFolder": true})
fileDialog.chosen.connect(function(path) {
if (!path) return
currentRoom.downloadFile(eventId, path + "/" + (content.filename || content.body))
})
fileDialog.open()
}
function downloadAndOpen() function downloadAndOpen()
{ {
@ -162,7 +186,7 @@ ColumnLayout {
else else
{ {
openOnFinished = true openOnFinished = true
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_") + (message || ".tmp")) currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + (message || ".tmp"))
} }
} }

View File

@ -9,6 +9,9 @@ import Spectral 0.1
import Spectral.Setting 0.1 import Spectral.Setting 0.1
import Spectral.Component 2.0 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 import Spectral.Font 0.1
ColumnLayout { ColumnLayout {
@ -16,13 +19,17 @@ ColumnLayout {
readonly property bool sentByMe: author === currentRoom.localUser readonly property bool sentByMe: author === currentRoom.localUser
property bool openOnFinished: false property bool openOnFinished: false
property bool showOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed readonly property bool downloaded: progressInfo && progressInfo.completed
id: root id: root
spacing: 0 spacing: 0
onDownloadedChanged: if (downloaded && openOnFinished) openSavedFile() onDownloadedChanged: {
if (downloaded && showOnFinished) showSavedFile()
if (downloaded && openOnFinished) openSavedFile()
}
Label { Label {
Layout.leftMargin: 48 Layout.leftMargin: 48
@ -52,6 +59,20 @@ ColumnLayout {
visible: avatarVisible visible: avatarVisible
hint: author.displayName hint: author.displayName
source: author.avatarMediaId source: author.avatarMediaId
Component {
id: userDetailDialog
UserDetailDialog {}
}
RippleEffect {
anchors.fill: parent
circular: true
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author}).open()
}
} }
Label { Label {
@ -69,16 +90,26 @@ ColumnLayout {
verticalAlignment: Label.AlignVCenter verticalAlignment: Label.AlignVCenter
} }
BusyIndicator {
Layout.preferredWidth: 64
Layout.preferredHeight: 64
visible: img.status == Image.Loading
}
Image { Image {
Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + messageRow.spacing : 0) - 48 Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + messageRow.spacing : 0) - 48
id: img id: img
source: downloaded ? progressInfo.localPath : "image://mxc/" + source: "image://mxc/" +
(content.info && content.info.thumbnail_info ? (content.info && content.info.thumbnail_info ?
content.thumbnailMediaId : content.mediaId) content.thumbnailMediaId : content.mediaId)
sourceSize.width: 200
sourceSize.height: 200 sourceSize.width: 720
sourceSize.height: 720
fillMode: Image.PreserveAspectCrop
layer.enabled: true layer.enabled: true
layer.effect: OpacityMask { layer.effect: OpacityMask {
@ -94,61 +125,110 @@ ColumnLayout {
color: "transparent" color: "transparent"
radius: 24 radius: 24
antialiasing: true
border.width: 2 border.width: 4
border.color: MPalette.banner border.color: MPalette.banner
} }
AutoMouseArea { 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 anchors.fill: parent
id: messageMouseArea id: messageMouseArea
onSecondaryClicked: messageContextMenu.popup() onPrimaryClicked: downloadAndShow()
Menu { onSecondaryClicked: {
id: messageContextMenu var contextMenu = imageDelegateContextMenu.createObject(ApplicationWindow.overlay)
contextMenu.viewSource.connect(function() {
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
})
contextMenu.downloadAndOpen.connect(downloadAndOpen)
contextMenu.saveFileAs.connect(saveFileAs)
contextMenu.reply.connect(function() {
roomPanelInput.replyUser = author
roomPanelInput.replyEventID = eventId
roomPanelInput.replyContent = message
roomPanelInput.isReply = true
roomPanelInput.focus()
})
contextMenu.redact.connect(function() {
currentRoom.redactEvent(eventId)
})
contextMenu.popup()
}
MenuItem { Component {
text: "View Source" id: messageSourceDialog
onTriggered: { MessageSourceDialog {}
sourceDialog.sourceText = toolTip }
sourceDialog.open()
}
}
MenuItem {
text: "Open Externally"
onTriggered: downloadAndOpen() Component {
} id: openFileDialog
MenuItem {
text: "Save As"
onTriggered: saveFileAs() OpenFileDialog {}
} }
MenuItem {
text: "Reply"
onTriggered: { Component {
roomPanelInput.replyUser = author id: imageDelegateContextMenu
roomPanelInput.replyEventID = eventId
roomPanelInput.replyContent = message
roomPanelInput.isReply = true
roomPanelInput.focus()
}
}
MenuItem {
text: "Redact"
onTriggered: currentRoom.redactEvent(eventId) FileDelegateContextMenu {}
} }
Component {
id: fullScreenImage
FullScreenImage {}
} }
} }
} }
} }
function saveFileAs() { currentRoom.saveFileAs(eventId) } function saveFileAs() {
var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay, {"selectFolder": true})
fileDialog.chosen.connect(function(path) {
if (!path) return
currentRoom.downloadFile(eventId, path + "/" + (content.filename || content.body))
})
fileDialog.open()
}
function downloadAndShow()
{
if (downloaded) showSavedFile()
else
{
showOnFinished = true
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + (message || ".tmp"))
}
}
function showSavedFile()
{
fullScreenImage.createObject(parent, {"eventId": eventId, "localPath": progressInfo.localPath}).show()
}
function downloadAndOpen() function downloadAndOpen()
{ {
@ -156,7 +236,7 @@ ColumnLayout {
else else
{ {
openOnFinished = true openOnFinished = true
currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_") + (message || ".tmp")) currentRoom.downloadFile(eventId, Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + eventId.replace(":", "_").replace("/", "_").replace("+", "_") + (message || ".tmp"))
} }
} }

View File

@ -7,7 +7,9 @@ import Spectral 0.1
import Spectral.Setting 0.1 import Spectral.Setting 0.1
import Spectral.Component 2.0 import Spectral.Component 2.0
import Spectral.Font 0.1 import Spectral.Dialog 2.0
import Spectral.Menu.Timeline 2.0
import Spectral.Effect 2.0
ColumnLayout { ColumnLayout {
readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote" || aboveEventType === "other") readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote" || aboveEventType === "other")
@ -48,6 +50,20 @@ ColumnLayout {
visible: avatarVisible visible: avatarVisible
hint: author.displayName hint: author.displayName
source: author.avatarMediaId source: author.avatarMediaId
Component {
id: userDetailDialog
UserDetailDialog {}
}
RippleEffect {
anchors.fill: parent
circular: true
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author}).open()
}
} }
Label { Label {
@ -74,43 +90,42 @@ ColumnLayout {
background: Rectangle { background: Rectangle {
color: sentByMe ? "#009DC2" : eventType === "notice" ? "#4285F4" : "#673AB7" color: sentByMe ? "#009DC2" : eventType === "notice" ? "#4285F4" : "#673AB7"
radius: 18 radius: 18
antialiasing: true
AutoMouseArea { AutoMouseArea {
anchors.fill: parent anchors.fill: parent
id: messageMouseArea id: messageMouseArea
onSecondaryClicked: messageContextMenu.popup() onSecondaryClicked: {
var contextMenu = messageDelegateContextMenu.createObject(ApplicationWindow.overlay)
contextMenu.viewSource.connect(function() {
messageSourceDialog.createObject(ApplicationWindow.overlay, {"sourceText": toolTip}).open()
})
contextMenu.reply.connect(function() {
roomPanelInput.replyUser = author
roomPanelInput.replyEventID = eventId
roomPanelInput.replyContent = contentLabel.selectedText || message
roomPanelInput.isReply = true
roomPanelInput.focus()
})
contextMenu.redact.connect(function() {
currentRoom.redactEvent(eventId)
})
contextMenu.popup()
}
Menu {
readonly property string selectedText: contentLabel.selectedText
id: messageContextMenu Component {
id: messageDelegateContextMenu
MenuItem { MessageDelegateContextMenu {}
text: "View Source" }
onTriggered: { Component {
sourceDialog.sourceText = toolTip id: messageSourceDialog
sourceDialog.open()
}
}
MenuItem {
text: "Reply"
onTriggered: { MessageSourceDialog {}
roomPanelInput.replyUser = author
roomPanelInput.replyEventID = eventId
roomPanelInput.replyContent = messageContextMenu.selectedText || message
roomPanelInput.isReply = true
roomPanelInput.focus()
}
}
MenuItem {
text: "Redact"
onTriggered: currentRoom.redactEvent(eventId)
}
} }
} }
} }
@ -123,19 +138,10 @@ ColumnLayout {
padding: 8 padding: 8
background: Item { background: RippleEffect {
Rectangle { anchors.fill: parent
anchors.leftMargin: 0
width: 2
height: parent.height
color: "white" onPrimaryClicked: goToEvent(replyEventId)
}
MouseArea {
anchors.fill: parent
onClicked: goToEvent(replyEventId)
}
} }
contentItem: RowLayout { contentItem: RowLayout {
@ -169,7 +175,7 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
color: "white" color: "white"
text: replyDisplay || "" text: "<style>a{color: white;} .user-pill{}</style>" + (replyDisplay || "")
wrapMode: Label.Wrap wrapMode: Label.Wrap
textFormat: Label.RichText textFormat: Label.RichText
@ -178,6 +184,14 @@ ColumnLayout {
} }
} }
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
visible: replyEventId || ""
color: "white"
}
TextEdit { TextEdit {
Layout.fillWidth: true Layout.fillWidth: true
@ -187,7 +201,7 @@ ColumnLayout {
color: "white" color: "white"
font.family: CommonFont.font.family font.family: window.font.family
font.pixelSize: 14 font.pixelSize: 14
selectByMouse: true selectByMouse: true
readOnly: true readOnly: true

View File

@ -23,5 +23,6 @@ Label {
background: Rectangle { background: Rectangle {
color: MPalette.banner color: MPalette.banner
radius: 4 radius: 4
antialiasing: true
} }
} }

View File

@ -5,5 +5,5 @@ SideNavButton 2.0 SideNavButton.qml
ScrollHelper 2.0 ScrollHelper.qml ScrollHelper 2.0 ScrollHelper.qml
AutoListView 2.0 AutoListView.qml AutoListView 2.0 AutoListView.qml
AutoTextField 2.0 AutoTextField.qml AutoTextField 2.0 AutoTextField.qml
SplitView 2.0 SplitView.qml
Avatar 2.0 Avatar.qml Avatar 2.0 Avatar.qml
FullScreenImage 2.0 FullScreenImage.qml

View File

@ -0,0 +1,50 @@
import 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()
}

View File

@ -0,0 +1,345 @@
import 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
Repeater {
model: AccountListModel{
controller: spectralController
}
delegate: Item {
width: detailColumn.width
height: 72
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
Avatar {
Layout.preferredWidth: height
Layout.fillHeight: true
source: user.avatarMediaId
hint: user.displayName || "No Name"
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
Label {
Layout.fillWidth: true
text: user.displayName || "No Name"
color: MPalette.foreground
font.pixelSize: 16
font.bold: true
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
text: connection === spectralController.connection ? "Active" : "Online"
color: MPalette.lighter
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
Menu {
id: contextMenu
MenuItem {
text: "Logout"
onClicked: spectralController.logout(connection)
}
}
RippleEffect {
anchors.fill: parent
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: "\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()
}
}
Control {
width: parent.width
contentItem: RowLayout {
MaterialIcon {
Layout.preferredWidth: 48
Layout.preferredHeight: 48
color: MPalette.foreground
icon: "\ue8aa"
}
Label {
Layout.fillWidth: true
color: MPalette.foreground
text: "Chat Background"
}
}
RippleEffect {
anchors.fill: parent
onPrimaryClicked: {
var fileDialog = chatBackgroundDialog.createObject(ApplicationWindow.overlay)
fileDialog.chosen.connect(function(path) {
if (!path) return
MSettings.timelineBackground = path
})
fileDialog.rejected.connect(function(path) {
MSettings.timelineBackground = ""
})
fileDialog.open()
}
}
}
}
onClosed: destroy()
}

View File

@ -0,0 +1,38 @@
import 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()
}

View File

@ -0,0 +1,30 @@
import 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()
}

View File

@ -0,0 +1,28 @@
import 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()
}

View File

@ -0,0 +1,38 @@
import 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()
}

View File

@ -0,0 +1,56 @@
import 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: root.accept()
}
}
function doLogin() {
spectralController.loginWithCredentials(serverField.text, usernameField.text, passwordField.text)
}
onClosed: destroy()
}

View File

@ -0,0 +1,27 @@
import 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()
}

View File

@ -0,0 +1,13 @@
import QtQuick 2.12
import QtQuick.Dialogs 1.2
FileDialog {
signal chosen(string path)
id: root
title: "Please choose a file"
selectMultiple: false
onAccepted: chosen(selectFolder ? folder : fileUrl)
}

View File

@ -0,0 +1,218 @@
import 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 ? 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 ? room.displayName : "No name"
source: room ? room.avatarMediaId : null
}
ColumnLayout {
Layout.fillWidth: true
Layout.margins: 4
AutoTextField {
Layout.fillWidth: true
text: room ? room.name : ""
placeholderText: "Room Name"
}
AutoTextField {
Layout.fillWidth: true
text: room ? room.topic : ""
placeholderText: "Room Topic"
}
}
}
Control {
Layout.fillWidth: true
visible: room ? room.predecessorId : false
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 ? room.successorId : false
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()
}
}
}
}
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
model: room ? room.aliases : null
currentIndex: room ? room.aliases.indexOf(room.canonicalAlias) : -1
}
}
RowLayout {
Layout.fillWidth: true
Label {
Layout.preferredWidth: 100
Layout.alignment: Qt.AlignTop
wrapMode: Label.Wrap
text: "Aliases"
color: MPalette.lighter
}
ColumnLayout {
Layout.fillWidth: true
Repeater {
model: room ? room.aliases : null
delegate: Label {
Layout.fillWidth: true
text: modelData
font.pixelSize: 12
color: MPalette.lighter
}
}
}
}
}
}
onClosed: destroy()
}

View File

@ -0,0 +1,164 @@
import 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
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: user ? user.displayName : "No name"
source: user ? user.avatarMediaId : null
}
ColumnLayout {
Layout.fillWidth: true
Label {
Layout.fillWidth: true
font.pixelSize: 18
font.bold: true
elide: Text.ElideRight
wrapMode: Text.NoWrap
text: user ? user.displayName : "No Name"
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 ? user.id : "No 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)
}
}
}
onClosed: destroy()
}

View File

@ -0,0 +1,12 @@
module 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

View File

@ -1,5 +0,0 @@
pragma Singleton
import QtQuick 2.12
import QtQuick.Controls 2.12
Label {}

View File

@ -1,3 +1,2 @@
module Spectral.Font module Spectral.Font
singleton MaterialFont 0.1 MaterialFont.qml singleton MaterialFont 0.1 MaterialFont.qml
singleton CommonFont 0.1 CommonFont.qml

View File

@ -0,0 +1,42 @@
import 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()
}

View File

@ -0,0 +1,46 @@
import 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()
}

View File

@ -0,0 +1,34 @@
import 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
MenuItem {
text: "View Source"
onTriggered: viewSource()
}
MenuItem {
text: "Reply"
onTriggered: reply()
}
MenuItem {
text: "Redact"
onTriggered: redact()
}
onClosed: destroy()
}

View File

@ -0,0 +1,3 @@
module Spectral.Menu.Timeline
MessageDelegateContextMenu 2.0 MessageDelegateContextMenu.qml
FileDelegateContextMenu 2.0 FileDelegateContextMenu.qml

View File

@ -0,0 +1,2 @@
module Spectral.Menu
RoomListContextMenu 2.0 RoomListContextMenu.qml

View File

@ -1,4 +0,0 @@
module Spectral.Page
Login 2.0 Login.qml
Room 2.0 Room.qml
Setting 2.0 Setting.qml

View File

@ -4,6 +4,9 @@ import QtQuick.Controls.Material 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import Spectral.Component 2.0 import Spectral.Component 2.0
import Spectral.Dialog 2.0
import Spectral.Effect 2.0
import Spectral.Setting 0.1
import Spectral 0.1 import Spectral 0.1
@ -16,79 +19,142 @@ Drawer {
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 32 anchors.margins: 24
Avatar { RowLayout {
Layout.preferredWidth: 96
Layout.preferredHeight: 96
Layout.alignment: Qt.AlignHCenter
hint: room ? room.displayName : "No name"
source: room ? room.avatarMediaId : null
}
Label {
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Label.Wrap spacing: 16
horizontalAlignment: Text.AlignHCenter
text: room && room.id ? room.id : "" Avatar {
Layout.preferredWidth: 72
Layout.preferredHeight: 72
hint: room ? room.displayName : "No name"
source: room ? room.avatarMediaId : null
}
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
}
}
} }
Label { MenuSeparator {
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Label.Wrap
horizontalAlignment: Text.AlignHCenter
text: room && room.canonicalAlias ? room.canonicalAlias : "No Canonical Alias"
} }
Label { Control {
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Label.Wrap padding: 0
horizontalAlignment: Text.AlignHCenter
text: room ? room.totalMemberCount + " Members" : "No Member Count" contentItem: RowLayout {
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.canonicalAlias ? room.canonicalAlias : "No Canonical Alias"
color: MPalette.accent
}
Label {
Layout.fillWidth: true
wrapMode: Label.Wrap
text: "Main Alias"
color: MPalette.lighter
}
Label {
Layout.fillWidth: true
wrapMode: Label.Wrap
text: room && room.topic ? room.topic : "No Topic"
color: MPalette.foreground
}
Label {
Layout.fillWidth: true
wrapMode: Label.Wrap
text: "Topic"
color: MPalette.lighter
}
}
}
background: RippleEffect {
onPrimaryClicked: roomSettingDialog.createObject(ApplicationWindow.overlay, {"room": room}).open()
}
}
MenuSeparator {
Layout.fillWidth: true
} }
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
AutoTextField { spacing: 8
MaterialIcon {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
icon: "\ue7ff"
color: MPalette.lighter
}
Label {
Layout.fillWidth: true Layout.fillWidth: true
id: roomNameField wrapMode: Label.Wrap
text: room && room.name ? room.name : "" text: room ? room.totalMemberCount + " Members" : "No Member Count"
color: MPalette.lighter
} }
ItemDelegate { ToolButton {
Layout.preferredWidth: height Layout.preferredWidth: 32
Layout.preferredHeight: parent.height Layout.preferredHeight: 32
contentItem: MaterialIcon { icon: "\ue5ca" } contentItem: MaterialIcon {
icon: "\ue145"
color: MPalette.lighter
}
onClicked: room.setName(roomNameField.text) onClicked: inviteUserDialog.createObject(ApplicationWindow.overlay, {"room": room}).open()
}
}
RowLayout {
Layout.fillWidth: true
AutoTextField {
Layout.fillWidth: true
id: roomTopicField
text: room && room.topic ? room.topic : ""
}
ItemDelegate {
Layout.preferredWidth: height
Layout.preferredHeight: parent.height
contentItem: MaterialIcon { icon: "\ue5ca" }
onClicked: room.setTopic(roomTopicField.text)
} }
} }
@ -106,7 +172,7 @@ Drawer {
room: roomDrawer.room room: roomDrawer.room
} }
delegate: SwipeDelegate { delegate: Item {
width: userListView.width width: userListView.width
height: 48 height: 48
@ -127,62 +193,36 @@ Drawer {
Layout.fillWidth: true Layout.fillWidth: true
text: name text: name
color: MPalette.foreground
} }
} }
swipe.right: Rectangle { RippleEffect {
width: height anchors.fill: parent
height: parent.height
anchors.right: parent.right
color: Material.accent
MaterialIcon { onPrimaryClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": room, "user": user}).open()
anchors.fill: parent
icon: "\ue879"
color: "white"
}
SwipeDelegate.onClicked: {
room.kickMember(userId)
swipe.close()
}
} }
onClicked: swipe.open(SwipeDelegate.Right)
} }
ScrollBar.vertical: ScrollBar {} ScrollBar.vertical: ScrollBar {}
} }
}
Button { Component {
Layout.fillWidth: true id: roomSettingDialog
text: "Invite User" RoomSettingsDialog {}
flat: true }
highlighted: true
onClicked: inviteUserDialog.open() Component {
id: userDetailDialog
Dialog { UserDetailDialog {}
x: (window.width - width) / 2 }
y: (window.height - height) / 2
width: 360
id: inviteUserDialog Component {
id: inviteUserDialog
parent: ApplicationWindow.overlay InviteUserDialog {}
title: "Input User ID"
modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
contentItem: AutoTextField {
id: inviteUserDialogTextField
placeholderText: "@bot:matrix.org"
}
onAccepted: room.inviteToRoom(inviteUserDialogTextField.text)
}
}
} }
} }

View File

@ -5,6 +5,8 @@ import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12 import QtQuick.Controls.Material 2.12
import Spectral.Component 2.0 import Spectral.Component 2.0
import Spectral.Dialog 2.0
import Spectral.Menu 2.0
import Spectral.Effect 2.0 import Spectral.Effect 2.0
import Spectral 0.1 import Spectral 0.1
@ -13,12 +15,11 @@ import Spectral.Setting 0.1
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
Item { Item {
property var controller: null property var connection: null
readonly property var user: controller.connection ? controller.connection.localUser : null readonly property var user: connection ? connection.localUser : null
property int filter: 0 property int filter: 0
property var enteredRoom: null property var enteredRoom: null
property alias errorControl: errorControl
signal enterRoom(var room) signal enterRoom(var room)
signal leaveRoom(var room) signal leaveRoom(var room)
@ -28,7 +29,7 @@ Item {
RoomListModel { RoomListModel {
id: roomListModel id: roomListModel
connection: controller.connection connection: root.connection
onNewMessage: if (!window.active && MSettings.showNotification) spectralController.postNotification(roomId, eventId, roomName, senderName, text, icon) onNewMessage: if (!window.active && MSettings.showNotification) spectralController.postNotification(roomId, eventId, roomName, senderName, text, icon)
} }
@ -44,8 +45,8 @@ Item {
switch (category) { switch (category) {
case 1: return "Invited" case 1: return "Invited"
case 2: return "Favorites" case 2: return "Favorites"
case 3: return "Rooms" case 3: return "People"
case 4: return "People" case 4: return "Rooms"
case 5: return "Low Priority" case 5: return "Low Priority"
} }
} }
@ -60,6 +61,9 @@ Item {
] ]
filters: [ filters: [
ExpressionFilter {
expression: joinState != "upgraded"
},
RegExpFilter { RegExpFilter {
roleName: "name" roleName: "name"
pattern: searchField.text pattern: searchField.text
@ -71,483 +75,15 @@ Item {
}, },
ExpressionFilter { ExpressionFilter {
enabled: filter === 2 enabled: filter === 2
expression: category === 1 || category === 2 || category === 4 expression: category === 1 || category === 2 || category === 3
}, },
ExpressionFilter { ExpressionFilter {
enabled: filter === 3 enabled: filter === 3
expression: category === 3 || category === 5 expression: category === 4 || category === 5
} }
] ]
} }
Drawer {
width: Math.max(root.width, 400)
height: root.height
id: drawer
edge: Qt.LeftEdge
Component {
id: mainPage
ColumnLayout {
readonly property string title: "Main"
id: mainColumn
spacing: 0
Control {
Layout.fillWidth: true
Layout.preferredHeight: 330
padding: 24
contentItem: ColumnLayout {
spacing: 4
Avatar {
Layout.preferredWidth: 200
Layout.preferredHeight: 200
Layout.margins: 12
Layout.alignment: Qt.AlignHCenter
source: root.user ? root.user.avatarMediaId : null
hint: root.user ? root.user.displayName : "?"
}
Label {
Layout.alignment: Qt.AlignHCenter
text: root.user ? root.user.displayName : "No Name"
color: "white"
font.pixelSize: 22
}
Label {
Layout.alignment: Qt.AlignHCenter
text: root.user ? root.user.id : "@example:matrix.org"
color: "white"
opacity: 0.7
font.pixelSize: 13
}
}
background: Rectangle { color: Material.primary }
RippleEffect {
anchors.fill: parent
onClicked: stackView.push(userPage)
}
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ColumnLayout {
width: mainColumn.width
spacing: 0
Repeater {
model: AccountListModel {
controller: spectralController
}
delegate: ItemDelegate {
Layout.fillWidth: true
text: user.displayName
onClicked: {
controller.connection = connection
drawer.close()
}
}
}
ItemDelegate {
Layout.fillWidth: true
text: "Add Account"
onClicked: loginDialog.open()
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: MSettings.darkTheme ? "#424242" : "#e7ebeb"
}
ItemDelegate {
Layout.fillWidth: true
text: "Settings"
onClicked: stackView.push(settingsPage)
}
ItemDelegate {
Layout.fillWidth: true
text: "Logout"
onClicked: controller.logout(controller.connection)
}
ItemDelegate {
Layout.fillWidth: true
text: "Exit"
onClicked: Qt.quit()
}
}
}
}
}
Component {
id: userPage
ScrollView {
readonly property string title: "User Info"
id: main
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ColumnLayout {
width: main.width
spacing: 0
ItemDelegate {
Layout.fillWidth: true
padding: 24
contentItem: ColumnLayout {
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: "Matrix ID"
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: root.user.id
color: "#5B7480"
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
ItemDelegate {
Layout.fillWidth: true
padding: 24
contentItem: ColumnLayout {
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: "Name"
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: root.user.name
color: "#5B7480"
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
ItemDelegate {
Layout.fillWidth: true
padding: 24
contentItem: ColumnLayout {
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: "Avatar"
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: root.user.avatarMediaId
color: "#5B7480"
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
ItemDelegate {
Layout.fillWidth: true
padding: 24
contentItem: ColumnLayout {
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: "Server"
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: root.controller.connection.accessToken
color: "#5B7480"
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
ItemDelegate {
Layout.fillWidth: true
padding: 24
contentItem: ColumnLayout {
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: "Device"
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: root.controller.connection.deviceId
color: "#5B7480"
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
ItemDelegate {
Layout.fillWidth: true
padding: 24
contentItem: ColumnLayout {
spacing: 0
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: "Token"
font.pixelSize: 16
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Label {
Layout.fillWidth: true
Layout.fillHeight: true
text: root.controller.connection.accessToken
color: "#5B7480"
font.pixelSize: 13
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
}
}
}
Component {
id: settingsPage
ScrollView {
readonly property string title: "Settings"
id: main
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
padding: 32
ColumnLayout {
width: main.width - 64
spacing: 0
Switch {
text: "Dark theme"
checked: MSettings.darkTheme
onCheckedChanged: MSettings.darkTheme = checked
}
Switch {
text: "Show notifications"
checked: MSettings.showNotification
onCheckedChanged: MSettings.showNotification = checked
}
Switch {
text: "Use press and hold instead of right click"
checked: MSettings.pressAndHold
onCheckedChanged: MSettings.pressAndHold = checked
}
Switch {
text: "Show tray icon"
checked: MSettings.showTray
onCheckedChanged: MSettings.showTray = checked
}
Switch {
text: "Enable timeline background"
checked: MSettings.enableTimelineBackground
onCheckedChanged: MSettings.enableTimelineBackground = checked
}
RowLayout {
Layout.fillWidth: true
Label {
text: "DPI"
}
Slider {
Layout.fillWidth: true
value: controller.dpi()
from: 100
to: 300
stepSize: 25
snapMode: Slider.SnapAlways
ToolTip.visible: pressed
ToolTip.text: value
onMoved: controller.setDpi(value)
}
}
}
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 64
visible: stackView.depth > 1
color: Material.primary
RowLayout {
anchors.fill: parent
anchors.margins: 4
ToolButton {
Layout.preferredWidth: height
Layout.fillHeight: true
contentItem: MaterialIcon {
icon: "\ue5c4"
color: "white"
}
onClicked: stackView.pop()
}
Label {
Layout.fillWidth: true
text: stackView.currentItem.title
color: "white"
font.pixelSize: 18
elide: Label.ElideRight
}
}
}
StackView {
Layout.fillWidth: true
Layout.fillHeight: true
id: stackView
clip: true
initialItem: mainPage
}
}
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
@ -564,7 +100,7 @@ Item {
rightPadding: 18 rightPadding: 18
contentItem: RowLayout { contentItem: RowLayout {
ItemDelegate { ToolButton {
Layout.preferredWidth: height Layout.preferredWidth: height
Layout.fillHeight: true Layout.fillHeight: true
@ -614,7 +150,7 @@ Item {
onClicked: filterMenu.popup() onClicked: filterMenu.popup()
} }
ItemDelegate { ToolButton {
Layout.preferredWidth: height Layout.preferredWidth: height
Layout.fillHeight: true Layout.fillHeight: true
@ -627,40 +163,14 @@ Item {
AutoTextField { AutoTextField {
readonly property bool active: text readonly property bool active: text
readonly property bool isRoom: text.match(/#.*:.*\..*/g) || text.match(/!.*:.*\..*/g)
readonly property bool isUser: text.match(/@.*:.*\..*/g)
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.alignment: Qt.AlignVCenter
id: searchField id: searchField
topPadding: 0
bottomPadding: 0
placeholderText: "Search..." placeholderText: "Search..."
color: MPalette.lighter color: MPalette.lighter
background: Item {}
}
ItemDelegate {
Layout.preferredWidth: height
Layout.fillHeight: true
visible: searchField.isRoom || searchField.isUser
contentItem: MaterialIcon { icon: "\ue145" }
onClicked: {
if (searchField.isRoom) {
controller.joinRoom(controller.connection, searchField.text)
return
}
if (searchField.isUser) {
controller.createDirectChat(controller.connection, searchField.text)
return
}
}
} }
Avatar { Avatar {
@ -673,9 +183,12 @@ Item {
source: root.user ? root.user.avatarMediaId : null source: root.user ? root.user.avatarMediaId : null
hint: root.user ? root.user.displayName : "?" hint: root.user ? root.user.displayName : "?"
MouseArea { RippleEffect {
anchors.fill: parent anchors.fill: parent
onClicked: drawer.open()
circular: true
onClicked: accountDetailDialog.createObject(ApplicationWindow.overlay).open()
} }
} }
} }
@ -692,52 +205,6 @@ Item {
} }
} }
Control {
property string error: ""
property string detail: ""
Layout.fillWidth: true
id: errorControl
visible: false
topPadding: 16
bottomPadding: 16
leftPadding: 24
rightPadding: 24
contentItem: ColumnLayout {
Label {
Layout.fillWidth: true
text: errorControl.error
font.pixelSize: 16
color: "white"
wrapMode: Text.Wrap
}
Label {
Layout.fillWidth: true
text: errorControl.detail
font.pixelSize: 14
color: "white"
opacity: 0.6
wrapMode: Text.Wrap
}
}
background: Rectangle {
color: "#273338"
}
RippleEffect {
anchors.fill: parent
onClicked: errorControl.visible = false
}
}
AutoListView { AutoListView {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
@ -766,7 +233,7 @@ Item {
} }
Rectangle { Rectangle {
width: unreadCount > 0 ? 4 : 0 width: unreadCount >= 0 ? 4 : 0
height: parent.height height: parent.height
color: Material.accent color: Material.accent
@ -854,11 +321,9 @@ Item {
RippleEffect { RippleEffect {
anchors.fill: parent anchors.fill: parent
onSecondaryClicked: roomContextMenu.popup()
onPrimaryClicked: { onPrimaryClicked: {
if (category === RoomType.Invited) { if (category === RoomType.Invited) {
inviteDialog.currentRoom = currentRoom acceptInvitationDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom}).open()
inviteDialog.open()
} else { } else {
if (enteredRoom) { if (enteredRoom) {
enteredRoom.displayed = false enteredRoom.displayed = false
@ -869,36 +334,13 @@ Item {
enteredRoom = currentRoom enteredRoom = currentRoom
} }
} }
onSecondaryClicked: roomListContextMenu.createObject(ApplicationWindow.overlay, {"room": currentRoom}).popup()
} }
Menu { Component {
id: roomContextMenu id: roomListContextMenu
MenuItem { RoomListContextMenu {}
text: "Favourite"
checkable: true
checked: category === RoomType.Favorite
onTriggered: category === RoomType.Favorite ? currentRoom.removeTag("m.favourite") : currentRoom.addTag("m.favourite", 1.0)
}
MenuItem {
text: "Deprioritize"
checkable: true
checked: category === RoomType.Deprioritized
onTriggered: category === RoomType.Deprioritized ? currentRoom.removeTag("m.lowpriority") : currentRoom.addTag("m.lowpriority", 1.0)
}
MenuSeparator {}
MenuItem {
text: "Mark as Read"
onTriggered: currentRoom.markAllMessagesAsRead()
}
MenuItem {
text: "Leave Room"
onTriggered: currentRoom.forget()
}
} }
} }
@ -917,42 +359,9 @@ Item {
} }
} }
Dialog { Component {
property var currentRoom id: acceptInvitationDialog
id: inviteDialog AcceptInvitationDialog {}
parent: ApplicationWindow.overlay
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 360
title: "Action Required"
modal: true
contentItem: Label { text: "Accept this invitation?" }
footer: DialogButtonBox {
Button {
text: "Accept"
flat: true
onClicked: currentRoom.acceptInvitation()
}
Button {
text: "Reject"
flat: true
onClicked: currentRoom.forget()
}
Button {
text: "Cancel"
flat: true
onClicked: inviteDialog.close()
}
}
} }
} }

View File

@ -23,27 +23,42 @@ Item {
room: currentRoom room: currentRoom
} }
RoomDrawer { Column {
width: Math.min(root.width * 0.7, 480)
height: root.height
id: roomDrawer
room: currentRoom
}
Label {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 16
visible: !currentRoom visible: !currentRoom
text: "Please choose a room."
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."
}
} }
Image { Image {
anchors.fill: parent anchors.fill: parent
visible: currentRoom && MSettings.enableTimelineBackground visible: currentRoom && MSettings.timelineBackground
source: MSettings.timelineBackground || MSettings.darkTheme ? "qrc:/assets/img/roompanel-dark.svg" : "qrc:/assets/img/roompanel.svg" source: MSettings.timelineBackground
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
} }
@ -64,7 +79,7 @@ Item {
topic: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : "" topic: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : ""
atTop: messageListView.atYBeginning atTop: messageListView.atYBeginning
onClicked: roomDrawer.open() onClicked: roomDrawer.visible ? roomDrawer.close() : roomDrawer.open()
} }
ColumnLayout { ColumnLayout {
@ -92,15 +107,16 @@ Item {
highlightMoveDuration: 500 highlightMoveDuration: 500
boundsBehavior: Flickable.DragOverBounds boundsBehavior: Flickable.DragOverBounds
model: SortFilterProxyModel { model: SortFilterProxyModel {
id: sortedMessageEventModel id: sortedMessageEventModel
sourceModel: messageEventModel sourceModel: messageEventModel
filters: ExpressionFilter { filters: [
expression: marks !== 0x10 && eventType !== "other" ExpressionFilter {
} expression: marks !== 0x10 && eventType !== "other"
}
]
onModelReset: { onModelReset: {
if (currentRoom) { if (currentRoom) {
@ -182,8 +198,7 @@ Item {
visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000 visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000
} }
MessageDelegate { MessageDelegate {}
}
} }
} }
@ -200,8 +215,7 @@ Item {
visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000 visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000
} }
MessageDelegate { MessageDelegate {}
}
} }
} }
@ -244,24 +258,21 @@ Item {
} }
} }
RoundButton { Button {
width: 64
height: 64
anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
id: goBottomFab
visible: currentRoom && currentRoom.hasUnreadMessages visible: currentRoom && currentRoom.hasUnreadMessages
contentItem: MaterialIcon { topPadding: 8
anchors.fill: parent bottomPadding: 8
leftPadding: 24
rightPadding: 24
icon: "\ue316" Material.foreground: MPalette.foreground
color: "white" Material.background: MPalette.banner
}
Material.background: Material.accent text: "Go to read marker"
onClicked: goToEvent(currentRoom.readMarkerEventId) onClicked: goToEvent(currentRoom.readMarkerEventId)
} }
@ -287,85 +298,6 @@ Item {
onClicked: messageListView.positionViewAtBeginning() onClicked: messageListView.positionViewAtBeginning()
} }
Popup {
property string sourceText
anchors.centerIn: parent
width: 480
id: sourceDialog
parent: ApplicationWindow.overlay
padding: 16
closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside
contentItem: ScrollView {
clip: true
TextArea {
readOnly: true
selectByMouse: true
text: sourceDialog.sourceText
}
}
}
Popup {
property alias listModel: readMarkerListView.model
x: (window.width - width) / 2
y: (window.height - height) / 2
width: 320
id: readMarkerDialog
parent: ApplicationWindow.overlay
modal: true
padding: 16
closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside
contentItem: AutoListView {
implicitHeight: Math.min(window.height - 64,
readMarkerListView.contentHeight)
id: readMarkerListView
clip: true
boundsBehavior: Flickable.DragOverBounds
delegate: ItemDelegate {
width: parent.width
height: 48
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 12
Avatar {
Layout.preferredWidth: height
Layout.fillHeight: true
source: modelData.avatar
hint: modelData.displayName
}
Label {
Layout.fillWidth: true
text: modelData.displayName
}
}
}
ScrollBar.vertical: ScrollBar {}
}
}
} }
Control { Control {

View File

@ -1,3 +1,4 @@
module Spectral.Panel module Spectral.Panel
RoomPanel 2.0 RoomPanel.qml RoomPanel 2.0 RoomPanel.qml
RoomListPanel 2.0 RoomListPanel.qml RoomListPanel 2.0 RoomListPanel.qml
RoomDrawer 2.0 RoomDrawer.qml

View File

@ -5,11 +5,11 @@ import Qt.labs.settings 1.0
Settings { Settings {
property bool showNotification: true property bool showNotification: true
property bool pressAndHold
property bool showTray: true property bool showTray: true
property bool darkTheme property bool darkTheme
property bool enableTimelineBackground: true
property string timelineBackground property string timelineBackground
property string fontFamily: "Roboto,Noto Sans,Noto Color Emoji"
} }

@ -1 +1 @@
Subproject commit b467b0816f5f6816778f90b55a9d0b5437310fd5 Subproject commit 52a81dfa8a5415be369d819837f445479b833cde

View File

@ -7,19 +7,21 @@ import Qt.labs.platform 1.0 as Platform
import Spectral.Panel 2.0 import Spectral.Panel 2.0
import Spectral.Component 2.0 import Spectral.Component 2.0
import Spectral.Page 2.0 import Spectral.Dialog 2.0
import Spectral.Effect 2.0 import Spectral.Effect 2.0
import Spectral 0.1 import Spectral 0.1
import Spectral.Setting 0.1 import Spectral.Setting 0.1
ApplicationWindow { ApplicationWindow {
readonly property bool inPortrait: window.width < window.height
Material.theme: MPalette.theme Material.theme: MPalette.theme
Material.background: MPalette.background Material.background: MPalette.background
width: 960 width: 960
height: 640 height: 640
minimumWidth: 720 minimumWidth: 480
minimumHeight: 360 minimumHeight: 360
id: window id: window
@ -27,6 +29,8 @@ ApplicationWindow {
visible: true visible: true
title: qsTr("Spectral") title: qsTr("Spectral")
font.family: MSettings.fontFamily
background: Rectangle { background: Rectangle {
color: MSettings.darkTheme ? "#303030" : "#FFFFFF" color: MSettings.darkTheme ? "#303030" : "#FFFFFF"
} }
@ -37,16 +41,14 @@ ApplicationWindow {
menu: Platform.Menu { menu: Platform.Menu {
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Hide Window") text: qsTr("Toggle Window")
onTriggered: hideWindow() onTriggered: window.visible ? hideWindow() : showWindow()
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Quit") text: qsTr("Quit")
onTriggered: Qt.quit() onTriggered: Qt.quit()
} }
} }
onActivated: showWindow()
} }
Controller { Controller {
@ -59,137 +61,105 @@ ApplicationWindow {
roomForm.goToEvent(eventId) roomForm.goToEvent(eventId)
showWindow() showWindow()
} }
onErrorOccured: { onErrorOccured: errorControl.show(error + ": " + detail, 3000)
roomListForm.errorControl.error = error
roomListForm.errorControl.detail = detail
roomListForm.errorControl.visible = true
}
onSyncDone: roomListForm.errorControl.visible = false
} }
Shortcut { Shortcut {
sequence: StandardKey.Quit sequence: "Ctrl+Q"
context: Qt.ApplicationShortcut
onActivated: Qt.quit() onActivated: Qt.quit()
} }
Dialog { ToolTip {
property bool busy: false id: errorControl
width: 360
x: (window.width - width) / 2
y: (window.height - height) / 2
id: loginDialog
parent: ApplicationWindow.overlay parent: ApplicationWindow.overlay
title: "Login" font.pixelSize: 14
contentItem: Column {
AutoTextField {
width: parent.width
id: serverField
placeholderText: "Server Address"
text: "https://matrix.org"
}
AutoTextField {
width: parent.width
id: usernameField
placeholderText: "Username"
}
AutoTextField {
width: parent.width
id: passwordField
placeholderText: "Password"
echoMode: TextInput.Password
}
}
footer: DialogButtonBox {
Button {
text: "OK"
flat: true
enabled: !loginDialog.busy
onClicked: loginDialog.doLogin()
}
Button {
text: "Cancel"
flat: true
enabled: !loginDialog.busy
onClicked: loginDialog.close()
}
ToolTip {
id: loginButtonTooltip
}
}
onVisibleChanged: {
if (visible) spectralController.onErrorOccured.connect(showError)
else spectralController.onErrorOccured.disconnect(showError)
}
function showError(error, detail) {
loginDialog.busy = false
loginButtonTooltip.text = error + ": " + detail
loginButtonTooltip.open()
}
function doLogin() {
if (!(serverField.text.startsWith("http") && serverField.text.includes("://"))) {
loginButtonTooltip.text = "Server address should start with http(s)://"
loginButtonTooltip.open()
return
}
loginDialog.busy = true
spectralController.loginWithCredentials(serverField.text, usernameField.text, passwordField.text)
spectralController.connectionAdded.connect(function(conn) {
busy = false
loginDialog.close()
})
}
} }
SplitView { Component {
anchors.fill: parent id: accountDetailDialog
AccountDetailDialog {}
}
Component {
id: loginDialog
LoginDialog {}
}
Component {
id: joinRoomDialog
JoinRoomDialog {}
}
Component {
id: createRoomDialog
CreateRoomDialog {}
}
Component {
id: fontFamilyDialog
FontFamilyDialog {}
}
Component {
id: chatBackgroundDialog
OpenFileDialog {}
}
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 { RoomListPanel {
width: window.width * 0.35 anchors.fill: parent
Layout.minimumWidth: 180
id: roomListForm id: roomListForm
clip: true clip: true
controller: spectralController connection: spectralController.connection
onLeaveRoom: roomForm.saveReadMarker(room) onLeaveRoom: roomForm.saveReadMarker(room)
} }
}
RoomPanel { RoomPanel {
Layout.fillWidth: true anchors.fill: parent
Layout.minimumWidth: 480 anchors.leftMargin: !inPortrait ? roomListDrawer.width : undefined
anchors.rightMargin: !inPortrait && roomDrawer.visible ? roomDrawer.width : undefined
id: roomForm id: roomForm
clip: true clip: true
currentRoom: roomListForm.enteredRoom 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 { Binding {
@ -208,9 +178,9 @@ ApplicationWindow {
window.hide() window.hide()
} }
Component.onCompleted: { Component.onCompleted: {
spectralController.initiated.connect(function() { spectralController.initiated.connect(function() {
if (spectralController.accountCount == 0) loginDialog.open() if (spectralController.accountCount == 0) loginDialog.createObject(window).open()
}) })
} }
} }

View File

@ -10,4 +10,3 @@ Theme=Light
Variant=Dense Variant=Dense
Primary=#344955 Primary=#344955
Accent=#673AB7 Accent=#673AB7
Font/Family="Roboto,Noto Sans,Noto Color Emoji"

23
res.qrc
View File

@ -13,7 +13,6 @@
<file>imports/Spectral/Component/qmldir</file> <file>imports/Spectral/Component/qmldir</file>
<file>imports/Spectral/Effect/ElevationEffect.qml</file> <file>imports/Spectral/Effect/ElevationEffect.qml</file>
<file>imports/Spectral/Effect/qmldir</file> <file>imports/Spectral/Effect/qmldir</file>
<file>imports/Spectral/Page/qmldir</file>
<file>assets/font/material.ttf</file> <file>assets/font/material.ttf</file>
<file>assets/img/icon.icns</file> <file>assets/img/icon.icns</file>
<file>assets/img/icon.ico</file> <file>assets/img/icon.ico</file>
@ -31,17 +30,31 @@
<file>imports/Spectral/Component/AutoListView.qml</file> <file>imports/Spectral/Component/AutoListView.qml</file>
<file>imports/Spectral/Component/AutoTextField.qml</file> <file>imports/Spectral/Component/AutoTextField.qml</file>
<file>imports/Spectral/Panel/RoomPanelInput.qml</file> <file>imports/Spectral/Panel/RoomPanelInput.qml</file>
<file>imports/Spectral/Component/SplitView.qml</file>
<file>imports/Spectral/Font/CommonFont.qml</file>
<file>imports/Spectral/Component/Timeline/SectionDelegate.qml</file> <file>imports/Spectral/Component/Timeline/SectionDelegate.qml</file>
<file>assets/img/roompanel.svg</file>
<file>assets/img/matrix.svg</file> <file>assets/img/matrix.svg</file>
<file>imports/Spectral/Effect/RippleEffect.qml</file> <file>imports/Spectral/Effect/RippleEffect.qml</file>
<file>imports/Spectral/Effect/CircleMask.qml</file> <file>imports/Spectral/Effect/CircleMask.qml</file>
<file>assets/img/roompanel-dark.svg</file>
<file>imports/Spectral/Component/Timeline/ImageDelegate.qml</file> <file>imports/Spectral/Component/Timeline/ImageDelegate.qml</file>
<file>imports/Spectral/Component/Avatar.qml</file> <file>imports/Spectral/Component/Avatar.qml</file>
<file>imports/Spectral/Setting/Palette.qml</file> <file>imports/Spectral/Setting/Palette.qml</file>
<file>imports/Spectral/Component/Timeline/FileDelegate.qml</file> <file>imports/Spectral/Component/Timeline/FileDelegate.qml</file>
<file>imports/Spectral/Component/FullScreenImage.qml</file>
<file>imports/Spectral/Dialog/qmldir</file>
<file>imports/Spectral/Dialog/RoomSettingsDialog.qml</file>
<file>imports/Spectral/Dialog/UserDetailDialog.qml</file>
<file>imports/Spectral/Dialog/MessageSourceDialog.qml</file>
<file>imports/Spectral/Dialog/LoginDialog.qml</file>
<file>imports/Spectral/Dialog/CreateRoomDialog.qml</file>
<file>imports/Spectral/Dialog/JoinRoomDialog.qml</file>
<file>imports/Spectral/Dialog/InviteUserDialog.qml</file>
<file>imports/Spectral/Dialog/AcceptInvitationDialog.qml</file>
<file>imports/Spectral/Menu/qmldir</file>
<file>imports/Spectral/Menu/RoomListContextMenu.qml</file>
<file>imports/Spectral/Menu/Timeline/qmldir</file>
<file>imports/Spectral/Menu/Timeline/MessageDelegateContextMenu.qml</file>
<file>imports/Spectral/Menu/Timeline/FileDelegateContextMenu.qml</file>
<file>imports/Spectral/Dialog/FontFamilyDialog.qml</file>
<file>imports/Spectral/Dialog/AccountDetailDialog.qml</file>
<file>imports/Spectral/Dialog/OpenFileDialog.qml</file>
</qresource> </qresource>
</RCC> </RCC>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -20,9 +20,6 @@ isEmpty(USE_SYSTEM_SORTFILTERPROXYMODEL) {
isEmpty(USE_SYSTEM_QMATRIXCLIENT) { isEmpty(USE_SYSTEM_QMATRIXCLIENT) {
USE_SYSTEM_QMATRIXCLIENT = false USE_SYSTEM_QMATRIXCLIENT = false
} }
isEmpty(BUNDLE_FONT) {
BUNDLE_FONT = false
}
$$USE_SYSTEM_QMATRIXCLIENT { $$USE_SYSTEM_QMATRIXCLIENT {
PKGCONFIG += QMatrixClient PKGCONFIG += QMatrixClient
@ -70,13 +67,6 @@ DEFINES += QT_DEPRECATED_WARNINGS
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
RESOURCES += res.qrc RESOURCES += res.qrc
$$BUNDLE_FONT {
message("Bundling fonts.")
DEFINES += BUNDLE_FONT
RESOURCES += font.qrc
} else {
message("Using fonts from operating system.")
}
# Additional import path used to resolve QML modules in Qt Creator's code model # Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH += imports/ QML_IMPORT_PATH += imports/

View File

@ -7,6 +7,7 @@
#include "events/eventcontent.h" #include "events/eventcontent.h"
#include "events/roommessageevent.h" #include "events/roommessageevent.h"
#include "csapi/account-data.h"
#include "csapi/joining.h" #include "csapi/joining.h"
#include "csapi/logout.h" #include "csapi/logout.h"
@ -15,6 +16,7 @@
#include <QClipboard> #include <QClipboard>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QSysInfo>
#include <QtCore/QDebug> #include <QtCore/QDebug>
#include <QtCore/QDir> #include <QtCore/QDir>
#include <QtCore/QElapsedTimer> #include <QtCore/QElapsedTimer>
@ -57,19 +59,25 @@ inline QString accessTokenFileName(const AccountSettings& account) {
'/' + fileName; '/' + fileName;
} }
void Controller::loginWithCredentials(QString serverAddr, QString user, void Controller::loginWithCredentials(QString serverAddr,
QString user,
QString pass) { QString pass) {
if (!user.isEmpty() && !pass.isEmpty()) { if (!user.isEmpty() && !pass.isEmpty()) {
QString deviceName = "Spectral " + QSysInfo::machineHostName() + " " +
QSysInfo::productType() + " " +
QSysInfo::productVersion() + " " +
QSysInfo::currentCpuArchitecture();
Connection* conn = new Connection(this); Connection* conn = new Connection(this);
conn->setHomeserver(QUrl(serverAddr)); conn->setHomeserver(QUrl(serverAddr));
conn->connectToServer(user, pass, ""); conn->connectToServer(user, pass, deviceName, "");
connect(conn, &Connection::connected, [=] { connect(conn, &Connection::connected, [=] {
AccountSettings account(conn->userId()); AccountSettings account(conn->userId());
account.setKeepLoggedIn(true); account.setKeepLoggedIn(true);
account.clearAccessToken(); // Drop the legacy - just in case account.clearAccessToken(); // Drop the legacy - just in case
account.setHomeserver(conn->homeserver()); account.setHomeserver(conn->homeserver());
account.setDeviceId(conn->deviceId()); account.setDeviceId(conn->deviceId());
account.setDeviceName("Spectral"); account.setDeviceName(deviceName);
if (!saveAccessToken(account, conn->accessToken())) if (!saveAccessToken(account, conn->accessToken()))
qWarning() << "Couldn't save access token"; qWarning() << "Couldn't save access token";
account.sync(); account.sync();
@ -100,7 +108,8 @@ void Controller::logout(Connection* conn) {
conn->stopSync(); conn->stopSync();
emit conn->stateChanged(); emit conn->stateChanged();
emit conn->loggedOut(); emit conn->loggedOut();
if (!m_connections.isEmpty()) setConnection(m_connections[0]); if (!m_connections.isEmpty())
setConnection(m_connections[0]);
}); });
connect(job, &LogoutJob::failure, this, [=] { connect(job, &LogoutJob::failure, this, [=] {
emit errorOccured("Server-side Logout Failed", job->errorString()); emit errorOccured("Server-side Logout Failed", job->errorString());
@ -160,14 +169,16 @@ void Controller::invokeLogin() {
c->connectWithToken(account.userId(), accessToken, account.deviceId()); c->connectWithToken(account.userId(), accessToken, account.deviceId());
} }
} }
if (!m_connections.isEmpty()) setConnection(m_connections[0]); if (!m_connections.isEmpty())
setConnection(m_connections[0]);
emit initiated(); emit initiated();
} }
QByteArray Controller::loadAccessToken(const AccountSettings& account) { QByteArray Controller::loadAccessToken(const AccountSettings& account) {
QFile accountTokenFile{accessTokenFileName(account)}; QFile accountTokenFile{accessTokenFileName(account)};
if (accountTokenFile.open(QFile::ReadOnly)) { if (accountTokenFile.open(QFile::ReadOnly)) {
if (accountTokenFile.size() < 1024) return accountTokenFile.readAll(); if (accountTokenFile.size() < 1024)
return accountTokenFile.readAll();
qWarning() << "File" << accountTokenFile.fileName() << "is" qWarning() << "File" << accountTokenFile.fileName() << "is"
<< accountTokenFile.size() << accountTokenFile.size()
@ -203,7 +214,8 @@ void Controller::joinRoom(Connection* c, const QString& alias) {
}); });
} }
void Controller::createRoom(Connection* c, const QString& name, void Controller::createRoom(Connection* c,
const QString& name,
const QString& topic) { const QString& topic) {
CreateRoomJob* createRoomJob = CreateRoomJob* createRoomJob =
c->createRoom(Connection::PublishRoom, "", name, topic, QStringList()); c->createRoom(Connection::PublishRoom, "", name, topic, QStringList());
@ -231,10 +243,12 @@ void Controller::playAudio(QUrl localFile) {
connect(player, &QMediaPlayer::stateChanged, [=] { player->deleteLater(); }); connect(player, &QMediaPlayer::stateChanged, [=] { player->deleteLater(); });
} }
void Controller::postNotification(const QString& roomId, const QString& eventId, void Controller::postNotification(const QString& roomId,
const QString& eventId,
const QString& roomName, const QString& roomName,
const QString& senderName, const QString& senderName,
const QString& text, const QImage& icon) { const QString& text,
const QImage& icon) {
notificationsManager.postNotification(roomId, eventId, roomName, senderName, notificationsManager.postNotification(roomId, eventId, roomName, senderName,
text, icon); text, icon);
} }

View File

@ -3,6 +3,7 @@
#include "connection.h" #include "connection.h"
#include "notifications/manager.h" #include "notifications/manager.h"
#include "room.h"
#include "settings.h" #include "settings.h"
#include "user.h" #include "user.h"
@ -53,13 +54,14 @@ class Controller : public QObject {
} }
Connection* connection() { Connection* connection() {
if (m_connection.isNull()) return nullptr; if (m_connection.isNull())
return nullptr;
return m_connection; return m_connection;
} }
void setConnection(Connection* conn) { void setConnection(Connection* conn) {
if (!conn) return; if (conn == m_connection)
if (conn == m_connection) return; return;
m_connection = conn; m_connection = conn;
emit connectionChanged(); emit connectionChanged();
} }
@ -99,9 +101,12 @@ class Controller : public QObject {
void createDirectChat(Connection* c, const QString& userID); void createDirectChat(Connection* c, const QString& userID);
void copyToClipboard(const QString& text); void copyToClipboard(const QString& text);
void playAudio(QUrl localFile); void playAudio(QUrl localFile);
void postNotification(const QString& roomId, const QString& eventId, void postNotification(const QString& roomId,
const QString& roomName, const QString& senderName, const QString& eventId,
const QString& text, const QImage& icon); const QString& roomName,
const QString& senderName,
const QString& text,
const QImage& icon);
}; };
#endif // CONTROLLER_H #endif // CONTROLLER_H

View File

@ -1,16 +1,26 @@
#include "imageprovider.h" #include "imageprovider.h"
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include <QtCore/QDebug> #include <QtCore/QDebug>
#include <QtCore/QThread> #include <QtCore/QThread>
using QMatrixClient::BaseJob; using QMatrixClient::BaseJob;
ThumbnailResponse::ThumbnailResponse(QMatrixClient::Connection* c, ThumbnailResponse::ThumbnailResponse(QMatrixClient::Connection* c,
QString id, const QSize& size) QString id,
: c(c), const QSize& size)
mediaId(std::move(id)), : c(c),
requestedSize(size), mediaId(std::move(id)),
errorStr("Image request hasn't started") { 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()) { if (requestedSize.isEmpty()) {
errorStr.clear(); errorStr.clear();
emit finished(); emit finished();
@ -18,11 +28,19 @@ ThumbnailResponse::ThumbnailResponse(QMatrixClient::Connection* c,
} }
if (mediaId.count('/') != 1) { if (mediaId.count('/') != 1) {
errorStr = errorStr =
tr("Media id '%1' doesn't follow server/mediaId pattern") tr("Media id '%1' doesn't follow server/mediaId pattern").arg(mediaId);
.arg(mediaId);
emit finished(); emit finished();
return; return;
} }
QImage cachedImage;
if (cachedImage.load(localFile)) {
image = cachedImage;
errorStr.clear();
emit finished();
return;
}
// Execute a request on the main thread asynchronously // Execute a request on the main thread asynchronously
moveToThread(c->thread()); moveToThread(c->thread());
QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest, QMetaObject::invokeMethod(this, &ThumbnailResponse::startRequest,
@ -45,6 +63,14 @@ void ThumbnailResponse::prepareResult() {
QWriteLocker _(&lock); QWriteLocker _(&lock);
if (job->error() == BaseJob::Success) { if (job->error() == BaseJob::Success) {
image = job->thumbnail(); image = job->thumbnail();
QString localPath = QFileInfo(localFile).absolutePath();
QDir dir;
if (!dir.exists(localPath))
dir.mkpath(localPath);
image.save(localFile);
errorStr.clear(); errorStr.clear();
} else if (job->error() == BaseJob::Abandoned) { } else if (job->error() == BaseJob::Abandoned) {
errorStr = tr("Image request has been cancelled"); errorStr = tr("Image request has been cancelled");
@ -83,7 +109,7 @@ void ThumbnailResponse::cancel() {
} }
QQuickImageResponse* ImageProvider::requestImageResponse( QQuickImageResponse* ImageProvider::requestImageResponse(
const QString& id, const QSize& requestedSize) { const QString& id,
qDebug() << "ImageProvider: requesting " << id << "of size" << requestedSize; const QSize& requestedSize) {
return new ThumbnailResponse(m_connection.load(), id, requestedSize); return new ThumbnailResponse(m_connection.load(), id, requestedSize);
} }

View File

@ -7,8 +7,8 @@
#include <connection.h> #include <connection.h>
#include <jobs/mediathumbnailjob.h> #include <jobs/mediathumbnailjob.h>
#include <QtCore/QReadWriteLock>
#include <QtCore/QAtomicPointer> #include <QtCore/QAtomicPointer>
#include <QtCore/QReadWriteLock>
namespace QMatrixClient { namespace QMatrixClient {
class Connection; class Connection;
@ -17,11 +17,12 @@ class Connection;
class ThumbnailResponse : public QQuickImageResponse { class ThumbnailResponse : public QQuickImageResponse {
Q_OBJECT Q_OBJECT
public: public:
ThumbnailResponse(QMatrixClient::Connection* c, QString mediaId, ThumbnailResponse(QMatrixClient::Connection* c,
QString mediaId,
const QSize& requestedSize); const QSize& requestedSize);
~ThumbnailResponse() override = default; ~ThumbnailResponse() override = default;
private slots: private slots:
void startRequest(); void startRequest();
void prepareResult(); void prepareResult();
void doCancel(); void doCancel();
@ -30,11 +31,12 @@ private slots:
QMatrixClient::Connection* c; QMatrixClient::Connection* c;
const QString mediaId; const QString mediaId;
const QSize requestedSize; const QSize requestedSize;
const QString localFile;
QMatrixClient::MediaThumbnailJob* job = nullptr; QMatrixClient::MediaThumbnailJob* job = nullptr;
QImage image; QImage image;
QString errorStr; QString errorStr;
mutable QReadWriteLock lock; // Guards ONLY these two members above mutable QReadWriteLock lock; // Guards ONLY these two members above
QQuickTextureFactory* textureFactory() const override; QQuickTextureFactory* textureFactory() const override;
QString errorString() const override; QString errorString() const override;
@ -49,7 +51,8 @@ class ImageProvider : public QObject, public QQuickAsyncImageProvider {
explicit ImageProvider() = default; explicit ImageProvider() = default;
QQuickImageResponse* requestImageResponse( QQuickImageResponse* requestImageResponse(
const QString& id, const QSize& requestedSize) override; const QString& id,
const QSize& requestedSize) override;
QMatrixClient::Connection* connection() { return m_connection; } QMatrixClient::Connection* connection() { return m_connection; }
void setConnection(QMatrixClient::Connection* connection) { void setConnection(QMatrixClient::Connection* connection) {

View File

@ -23,7 +23,7 @@
using namespace QMatrixClient; using namespace QMatrixClient;
int main(int argc, char *argv[]) { int main(int argc, char* argv[]) {
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN) || defined(Q_OS_FREEBSD) #if defined(Q_OS_LINUX) || defined(Q_OS_WIN) || defined(Q_OS_FREEBSD)
if (qgetenv("QT_SCALE_FACTOR").size() == 0) { if (qgetenv("QT_SCALE_FACTOR").size() == 0) {
QSettings settings("ENCOM", "Spectral"); QSettings settings("ENCOM", "Spectral");
@ -36,6 +36,8 @@ int main(int argc, char *argv[]) {
} }
#endif #endif
QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
QNetworkProxyFactory::setUseSystemConfiguration(true); QNetworkProxyFactory::setUseSystemConfiguration(true);
QApplication app(argc, argv); QApplication app(argc, argv);
@ -57,26 +59,24 @@ int main(int argc, char *argv[]) {
"RoomMessageEvent", "ENUM"); "RoomMessageEvent", "ENUM");
qmlRegisterUncreatableType<RoomType>("Spectral", 0, 1, "RoomType", "ENUM"); qmlRegisterUncreatableType<RoomType>("Spectral", 0, 1, "RoomType", "ENUM");
qRegisterMetaType<User *>("User*"); qRegisterMetaType<User*>("User*");
qRegisterMetaType<Room *>("Room*"); qRegisterMetaType<User*>("const User*");
qRegisterMetaType<Room*>("Room*");
qRegisterMetaType<Connection*>("Connection*");
qRegisterMetaType<MessageEventType>("MessageEventType"); qRegisterMetaType<MessageEventType>("MessageEventType");
qRegisterMetaType<SpectralRoom *>("SpectralRoom*"); qRegisterMetaType<SpectralRoom*>("SpectralRoom*");
qRegisterMetaType<SpectralUser *>("SpectralUser*"); qRegisterMetaType<SpectralUser*>("SpectralUser*");
#if defined(BUNDLE_FONT)
QFontDatabase::addApplicationFont(":/assets/font/roboto.ttf");
QFontDatabase::addApplicationFont(":/assets/font/twemoji.ttf");
#endif
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
engine.addImportPath("qrc:/imports"); engine.addImportPath("qrc:/imports");
ImageProvider *m_provider = new ImageProvider(); ImageProvider* m_provider = new ImageProvider();
engine.rootContext()->setContextProperty("imageProvider", m_provider); engine.rootContext()->setContextProperty("imageProvider", m_provider);
engine.addImageProvider(QLatin1String("mxc"), m_provider); engine.addImageProvider(QLatin1String("mxc"), m_provider);
engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml")));
if (engine.rootObjects().isEmpty()) return -1; if (engine.rootObjects().isEmpty())
return -1;
return app.exec(); return app.exec();
} }

View File

@ -41,7 +41,7 @@ QHash<int, QByteArray> MessageEventModel::roleNames() const {
return roles; return roles;
} }
MessageEventModel::MessageEventModel(QObject *parent) MessageEventModel::MessageEventModel(QObject* parent)
: QAbstractListModel(parent), m_currentRoom(nullptr) { : QAbstractListModel(parent), m_currentRoom(nullptr) {
using namespace QMatrixClient; using namespace QMatrixClient;
qmlRegisterType<FileTransferInfo>(); qmlRegisterType<FileTransferInfo>();
@ -52,8 +52,9 @@ MessageEventModel::MessageEventModel(QObject *parent)
MessageEventModel::~MessageEventModel() {} MessageEventModel::~MessageEventModel() {}
void MessageEventModel::setRoom(SpectralRoom *room) { void MessageEventModel::setRoom(SpectralRoom* room) {
if (room == m_currentRoom) return; if (room == m_currentRoom)
return;
beginResetModel(); beginResetModel();
if (m_currentRoom) { if (m_currentRoom) {
@ -96,8 +97,9 @@ void MessageEventModel::setRoom(SpectralRoom *room) {
connect(m_currentRoom, &Room::pendingEventAdded, this, connect(m_currentRoom, &Room::pendingEventAdded, this,
&MessageEventModel::endInsertRows); &MessageEventModel::endInsertRows);
connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, connect(m_currentRoom, &Room::pendingEventAboutToMerge, this,
[this](RoomEvent *, int i) { [this](RoomEvent*, int i) {
if (i == 0) return; // No need to move anything, just refresh if (i == 0)
return; // No need to move anything, just refresh
movingEvent = true; movingEvent = true;
// Reverse i because row 0 is bottommost in the model // Reverse i because row 0 is bottommost in the model
@ -131,7 +133,7 @@ void MessageEventModel::setRoom(SpectralRoom *room) {
refreshEventRoles(lastReadEventId, {ReadMarkerRole}); refreshEventRoles(lastReadEventId, {ReadMarkerRole});
}); });
connect(m_currentRoom, &Room::replacedEvent, this, connect(m_currentRoom, &Room::replacedEvent, this,
[this](const RoomEvent *newEvent) { [this](const RoomEvent* newEvent) {
refreshLastUserEvents(refreshEvent(newEvent->id()) - refreshLastUserEvents(refreshEvent(newEvent->id()) -
timelineBaseIndex()); timelineBaseIndex());
}); });
@ -144,10 +146,15 @@ void MessageEventModel::setRoom(SpectralRoom *room) {
connect(m_currentRoom, &Room::fileTransferCancelled, this, connect(m_currentRoom, &Room::fileTransferCancelled, this,
&MessageEventModel::refreshEvent); &MessageEventModel::refreshEvent);
connect(m_currentRoom, &Room::readMarkerForUserMoved, this, connect(m_currentRoom, &Room::readMarkerForUserMoved, this,
[=](User *, QString fromEventId, QString toEventId) { [=](User*, QString fromEventId, QString toEventId) {
refreshEventRoles(fromEventId, {UserMarkerRole}); refreshEventRoles(fromEventId, {UserMarkerRole});
refreshEventRoles(toEventId, {UserMarkerRole}); refreshEventRoles(toEventId, {UserMarkerRole});
}); });
connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged,
this, [=] {
beginResetModel();
endResetModel();
});
qDebug() << "Connected to room" << room->id() << "as" qDebug() << "Connected to room" << room->id() << "as"
<< room->localUser()->id(); << room->localUser()->id();
} else } else
@ -155,23 +162,25 @@ void MessageEventModel::setRoom(SpectralRoom *room) {
endResetModel(); endResetModel();
} }
int MessageEventModel::refreshEvent(const QString &eventId) { int MessageEventModel::refreshEvent(const QString& eventId) {
return refreshEventRoles(eventId); return refreshEventRoles(eventId);
} }
void MessageEventModel::refreshRow(int row) { refreshEventRoles(row); } void MessageEventModel::refreshRow(int row) {
refreshEventRoles(row);
}
int MessageEventModel::timelineBaseIndex() const { int MessageEventModel::timelineBaseIndex() const {
return m_currentRoom ? int(m_currentRoom->pendingEvents().size()) : 0; return m_currentRoom ? int(m_currentRoom->pendingEvents().size()) : 0;
} }
void MessageEventModel::refreshEventRoles(int row, const QVector<int> &roles) { void MessageEventModel::refreshEventRoles(int row, const QVector<int>& roles) {
const auto idx = index(row); const auto idx = index(row);
emit dataChanged(idx, idx, roles); emit dataChanged(idx, idx, roles);
} }
int MessageEventModel::refreshEventRoles(const QString &eventId, int MessageEventModel::refreshEventRoles(const QString& eventId,
const QVector<int> &roles) { const QVector<int>& roles) {
const auto it = m_currentRoom->findInTimeline(eventId); const auto it = m_currentRoom->findInTimeline(eventId);
if (it == m_currentRoom->timelineEdge()) { if (it == m_currentRoom->timelineEdge()) {
qWarning() << "Trying to refresh inexistent event:" << eventId; qWarning() << "Trying to refresh inexistent event:" << eventId;
@ -183,15 +192,16 @@ int MessageEventModel::refreshEventRoles(const QString &eventId,
return row; return row;
} }
inline bool hasValidTimestamp(const QMatrixClient::TimelineItem &ti) { inline bool hasValidTimestamp(const QMatrixClient::TimelineItem& ti) {
return ti->timestamp().isValid(); return ti->timestamp().isValid();
} }
QDateTime MessageEventModel::makeMessageTimestamp( QDateTime MessageEventModel::makeMessageTimestamp(
const QMatrixClient::Room::rev_iter_t &baseIt) const { const QMatrixClient::Room::rev_iter_t& baseIt) const {
const auto &timeline = m_currentRoom->messageEvents(); const auto& timeline = m_currentRoom->messageEvents();
auto ts = baseIt->event()->timestamp(); auto ts = baseIt->event()->timestamp();
if (ts.isValid()) return ts; if (ts.isValid())
return ts;
// The event is most likely redacted or just invalid. // The event is most likely redacted or just invalid.
// Look for the nearest date around and slap zero time to it. // Look for the nearest date around and slap zero time to it.
@ -210,11 +220,14 @@ QDateTime MessageEventModel::makeMessageTimestamp(
QString MessageEventModel::renderDate(QDateTime timestamp) const { QString MessageEventModel::renderDate(QDateTime timestamp) const {
auto date = timestamp.toLocalTime().date(); auto date = timestamp.toLocalTime().date();
if (date == QDate::currentDate()) return tr("Today"); if (date == QDate::currentDate())
if (date == QDate::currentDate().addDays(-1)) return tr("Yesterday"); return tr("Today");
if (date == QDate::currentDate().addDays(-1))
return tr("Yesterday");
if (date == QDate::currentDate().addDays(-2)) if (date == QDate::currentDate().addDays(-2))
return tr("The day before yesterday"); return tr("The day before yesterday");
if (date > QDate::currentDate().addDays(-7)) return date.toString("dddd"); if (date > QDate::currentDate().addDays(-7))
return date.toString("dddd");
return date.toString(Qt::DefaultLocaleShortDate); return date.toString(Qt::DefaultLocaleShortDate);
} }
@ -222,8 +235,8 @@ void MessageEventModel::refreshLastUserEvents(int baseTimelineRow) {
if (!m_currentRoom || m_currentRoom->timelineSize() <= baseTimelineRow) if (!m_currentRoom || m_currentRoom->timelineSize() <= baseTimelineRow)
return; return;
const auto &timelineBottom = m_currentRoom->messageEvents().rbegin(); const auto& timelineBottom = m_currentRoom->messageEvents().rbegin();
const auto &lastSender = (*(timelineBottom + baseTimelineRow))->senderId(); const auto& lastSender = (*(timelineBottom + baseTimelineRow))->senderId();
const auto limit = timelineBottom + std::min(baseTimelineRow + 10, const auto limit = timelineBottom + std::min(baseTimelineRow + 10,
m_currentRoom->timelineSize()); m_currentRoom->timelineSize());
for (auto it = timelineBottom + std::max(baseTimelineRow - 10, 0); for (auto it = timelineBottom + std::max(baseTimelineRow - 10, 0);
@ -235,12 +248,13 @@ void MessageEventModel::refreshLastUserEvents(int baseTimelineRow) {
} }
} }
int MessageEventModel::rowCount(const QModelIndex &parent) const { int MessageEventModel::rowCount(const QModelIndex& parent) const {
if (!m_currentRoom || parent.isValid()) return 0; if (!m_currentRoom || parent.isValid())
return 0;
return m_currentRoom->timelineSize(); return m_currentRoom->timelineSize();
} }
QVariant MessageEventModel::data(const QModelIndex &idx, int role) const { QVariant MessageEventModel::data(const QModelIndex& idx, int role) const {
const auto row = idx.row(); const auto row = idx.row();
if (!m_currentRoom || row < 0 || if (!m_currentRoom || row < 0 ||
@ -253,14 +267,14 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const {
std::max(0, row - timelineBaseIndex()); std::max(0, row - timelineBaseIndex());
const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + const auto pendingIt = m_currentRoom->pendingEvents().crbegin() +
std::min(row, timelineBaseIndex()); std::min(row, timelineBaseIndex());
const auto &evt = isPending ? **pendingIt : **timelineIt; const auto& evt = isPending ? **pendingIt : **timelineIt;
if (role == Qt::DisplayRole) { if (role == Qt::DisplayRole) {
return utils::removeReply(utils::eventToString(evt, m_currentRoom, Qt::RichText)); return utils::removeReply(m_currentRoom->eventToString(evt, Qt::RichText));
} }
if (role == MessageRole) { if (role == MessageRole) {
return utils::removeReply(utils::eventToString(evt, m_currentRoom)); return utils::removeReply(m_currentRoom->eventToString(evt));
} }
if (role == Qt::ToolTipRole) { if (role == Qt::ToolTipRole) {
@ -282,7 +296,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const {
return e->hasFileContent() ? "file" : "message"; return e->hasFileContent() ? "file" : "message";
} }
} }
if (evt.isStateEvent()) return "state"; if (evt.isStateEvent())
return "state";
return "other"; return "other";
} }
@ -298,7 +313,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const {
if (role == ContentTypeRole) { if (role == ContentTypeRole) {
if (auto e = eventCast<const RoomMessageEvent>(&evt)) { if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
const auto &contentType = e->mimeType().name(); const auto& contentType = e->mimeType().name();
return contentType == "text/plain" ? QStringLiteral("text/html") return contentType == "text/plain" ? QStringLiteral("text/html")
: contentType; : contentType;
} }
@ -323,19 +338,27 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const {
}; };
} }
if (role == HighlightRole) return m_currentRoom->isEventHighlighted(&evt); if (role == HighlightRole)
return m_currentRoom->isEventHighlighted(&evt);
if (role == ReadMarkerRole) if (role == ReadMarkerRole)
return evt.id() == lastReadEventId && row > timelineBaseIndex(); return evt.id() == lastReadEventId && row > timelineBaseIndex();
if (role == SpecialMarksRole) { if (role == SpecialMarksRole) {
if (isPending) return pendingIt->deliveryStatus(); if (isPending)
return pendingIt->deliveryStatus();
if (is<RedactionEvent>(evt)) return EventStatus::Hidden; if (is<RedactionEvent>(evt))
if (evt.isRedacted()) return EventStatus::Hidden; return EventStatus::Hidden;
if (evt.isRedacted())
return EventStatus::Hidden;
if (evt.isStateEvent() && if (evt.isStateEvent() &&
static_cast<const StateEventBase &>(evt).repeatsState()) static_cast<const StateEventBase&>(evt).repeatsState())
return EventStatus::Hidden;
if (m_currentRoom->connection()->isIgnored(
m_currentRoom->user(evt.senderId())))
return EventStatus::Hidden; return EventStatus::Hidden;
return EventStatus::Normal; return EventStatus::Normal;
@ -351,7 +374,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const {
} }
if (role == AnnotationRole) if (role == AnnotationRole)
if (isPending) return pendingIt->annotation(); if (isPending)
return pendingIt->annotation();
if (role == TimeRole || role == SectionRole) { if (role == TimeRole || role == SectionRole) {
auto ts = auto ts =
@ -361,8 +385,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const {
if (role == UserMarkerRole) { if (role == UserMarkerRole) {
QVariantList variantList; QVariantList variantList;
for (User *user : m_currentRoom->usersAtEventId(evt.id())) { for (User* user : m_currentRoom->usersAtEventId(evt.id())) {
if (user == m_currentRoom->localUser()) continue; if (user == m_currentRoom->localUser())
continue;
variantList.append(QVariant::fromValue(user)); variantList.append(QVariant::fromValue(user));
} }
return variantList; return variantList;
@ -370,22 +395,24 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const {
if (role == ReplyEventIdRole || role == ReplyDisplayRole || if (role == ReplyEventIdRole || role == ReplyDisplayRole ||
role == ReplyAuthorRole) { role == ReplyAuthorRole) {
const QString &replyEventId = evt.contentJson()["m.relates_to"] const QString& replyEventId = evt.contentJson()["m.relates_to"]
.toObject()["m.in_reply_to"] .toObject()["m.in_reply_to"]
.toObject()["event_id"] .toObject()["event_id"]
.toString(); .toString();
if (replyEventId.isEmpty()) return {}; if (replyEventId.isEmpty())
return {};
const auto replyIt = m_currentRoom->findInTimeline(replyEventId); const auto replyIt = m_currentRoom->findInTimeline(replyEventId);
if (replyIt == m_currentRoom->timelineEdge()) return {}; if (replyIt == m_currentRoom->timelineEdge())
return {};
const auto& replyEvt = **replyIt; const auto& replyEvt = **replyIt;
switch (role) { switch (role) {
case ReplyEventIdRole: case ReplyEventIdRole:
return replyEventId; return replyEventId;
case ReplyDisplayRole: case ReplyDisplayRole:
return utils::removeReply(utils::eventToString(replyEvt, m_currentRoom, Qt::RichText)); return utils::removeReply(
m_currentRoom->eventToString(replyEvt, Qt::RichText));
case ReplyAuthorRole: case ReplyAuthorRole:
return QVariant::fromValue( return QVariant::fromValue(m_currentRoom->user(replyEvt.senderId()));
m_currentRoom->user(replyEvt.senderId()));
} }
return {}; return {};
} }
@ -394,7 +421,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const {
role == AboveAuthorRole || role == AboveTimeRole) role == AboveAuthorRole || role == AboveTimeRole)
for (auto r = row + 1; r < rowCount(); ++r) { for (auto r = row + 1; r < rowCount(); ++r) {
auto i = index(r); auto i = index(r);
if (data(i, SpecialMarksRole) != EventStatus::Hidden) switch (role) { if (data(i, SpecialMarksRole) != EventStatus::Hidden)
switch (role) {
case AboveEventTypeRole: case AboveEventTypeRole:
return data(i, EventTypeRole); return data(i, EventTypeRole);
case AboveSectionRole: case AboveSectionRole:
@ -409,7 +437,7 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const {
return {}; return {};
} }
int MessageEventModel::eventIDToIndex(const QString &eventID) { int MessageEventModel::eventIDToIndex(const QString& eventID) {
const auto it = m_currentRoom->findInTimeline(eventID); const auto it = m_currentRoom->findInTimeline(eventID);
if (it == m_currentRoom->timelineEdge()) { if (it == m_currentRoom->timelineEdge()) {
qWarning() << "Trying to find inexistent event:" << eventID; qWarning() << "Trying to find inexistent event:" << eventID;

View File

@ -16,8 +16,10 @@ RoomListModel::RoomListModel(QObject* parent) : QAbstractListModel(parent) {}
RoomListModel::~RoomListModel() {} RoomListModel::~RoomListModel() {}
void RoomListModel::setConnection(Connection* connection) { void RoomListModel::setConnection(Connection* connection) {
if (connection == m_connection) return; if (connection == m_connection)
if (m_connection) m_connection->disconnect(this); return;
if (m_connection)
m_connection->disconnect(this);
if (!connection) { if (!connection) {
qDebug() << "Removing current connection..."; qDebug() << "Removing current connection...";
m_connection = nullptr; m_connection = nullptr;
@ -29,7 +31,8 @@ void RoomListModel::setConnection(Connection* connection) {
m_connection = connection; m_connection = connection;
for (SpectralRoom* room : m_rooms) room->disconnect(this); for (SpectralRoom* room : m_rooms)
room->disconnect(this);
connect(connection, &Connection::connected, this, connect(connection, &Connection::connected, this,
&RoomListModel::doResetModel); &RoomListModel::doResetModel);
@ -40,6 +43,12 @@ void RoomListModel::setConnection(Connection* connection) {
connect(connection, &Connection::leftRoom, this, &RoomListModel::updateRoom); connect(connection, &Connection::leftRoom, this, &RoomListModel::updateRoom);
connect(connection, &Connection::aboutToDeleteRoom, this, connect(connection, &Connection::aboutToDeleteRoom, this,
&RoomListModel::deleteRoom); &RoomListModel::deleteRoom);
connect(connection, &Connection::directChatsListChanged, this,
[=](Connection::DirectChatsMap additions,
Connection::DirectChatsMap removals) {
for (QString roomID : additions.values() + removals.values())
refresh(static_cast<SpectralRoom*>(connection->room(roomID)));
});
doResetModel(); doResetModel();
} }
@ -47,11 +56,14 @@ void RoomListModel::setConnection(Connection* connection) {
void RoomListModel::doResetModel() { void RoomListModel::doResetModel() {
beginResetModel(); beginResetModel();
m_rooms.clear(); m_rooms.clear();
for (auto r : m_connection->roomMap()) doAddRoom(r); for (auto r : m_connection->roomMap())
doAddRoom(r);
endResetModel(); endResetModel();
} }
SpectralRoom* RoomListModel::roomAt(int row) { return m_rooms.at(row); } SpectralRoom* RoomListModel::roomAt(int row) {
return m_rooms.at(row);
}
void RoomListModel::doAddRoom(Room* r) { void RoomListModel::doAddRoom(Room* r) {
if (auto* room = static_cast<SpectralRoom*>(r)) { if (auto* room = static_cast<SpectralRoom*>(r)) {
@ -77,16 +89,19 @@ void RoomListModel::connectRoomSignals(SpectralRoom* room) {
connect(room, &Room::addedMessages, this, connect(room, &Room::addedMessages, this,
[=] { refresh(room, {LastEventRole}); }); [=] { refresh(room, {LastEventRole}); });
connect(room, &Room::notificationCountChanged, this, [=] { connect(room, &Room::notificationCountChanged, this, [=] {
if (room->notificationCount() == 0) return; if (room->notificationCount() == 0)
if (room->timelineSize() == 0) return; return;
const RoomEvent* lastEvent = room->messageEvents().rbegin()->get(); if (room->timelineSize() == 0)
if (lastEvent->isStateEvent()) return; return;
User* sender = room->user(lastEvent->senderId()); const RoomEvent* lastEvent = room->messageEvents().rbegin()->get();
if (sender == room->localUser()) return; if (lastEvent->isStateEvent())
emit newMessage( return;
room->id(), lastEvent->id(), room->displayName(), User* sender = room->user(lastEvent->senderId());
sender->displayname(), utils::eventToString(*lastEvent), if (sender == room->localUser())
room->avatar(128)); return;
emit newMessage(room->id(), lastEvent->id(), room->displayName(),
sender->displayname(), room->eventToString(*lastEvent),
room->avatar(128));
}); });
} }
@ -129,7 +144,8 @@ void RoomListModel::updateRoom(Room* room, Room* prev) {
void RoomListModel::deleteRoom(Room* room) { void RoomListModel::deleteRoom(Room* room) {
qDebug() << "Deleting room" << room->id(); qDebug() << "Deleting room" << room->id();
const auto it = std::find(m_rooms.begin(), m_rooms.end(), room); const auto it = std::find(m_rooms.begin(), m_rooms.end(), room);
if (it == m_rooms.end()) return; // Already deleted, nothing to do if (it == m_rooms.end())
return; // Already deleted, nothing to do
qDebug() << "Erasing room" << room->id(); qDebug() << "Erasing room" << room->id();
const int row = it - m_rooms.begin(); const int row = it - m_rooms.begin();
beginRemoveRows(QModelIndex(), row, row); beginRemoveRows(QModelIndex(), row, row);
@ -138,34 +154,54 @@ void RoomListModel::deleteRoom(Room* room) {
} }
int RoomListModel::rowCount(const QModelIndex& parent) const { int RoomListModel::rowCount(const QModelIndex& parent) const {
if (parent.isValid()) return 0; if (parent.isValid())
return 0;
return m_rooms.count(); return m_rooms.count();
} }
QVariant RoomListModel::data(const QModelIndex& index, int role) const { QVariant RoomListModel::data(const QModelIndex& index, int role) const {
if (!index.isValid()) return QVariant(); if (!index.isValid())
return QVariant();
if (index.row() >= m_rooms.count()) { if (index.row() >= m_rooms.count()) {
qDebug() << "UserListModel: something wrong here..."; qDebug() << "UserListModel: something wrong here...";
return QVariant(); return QVariant();
} }
SpectralRoom* room = m_rooms.at(index.row()); SpectralRoom* room = m_rooms.at(index.row());
if (role == NameRole) return room->displayName(); if (role == NameRole)
if (role == AvatarRole) return room->avatarMediaId(); return room->displayName();
if (role == TopicRole) return room->topic(); if (role == AvatarRole)
return room->avatarMediaId();
if (role == TopicRole)
return room->topic();
if (role == CategoryRole) { if (role == CategoryRole) {
if (room->joinState() == JoinState::Invite) return RoomType::Invited; if (room->joinState() == JoinState::Invite)
if (room->isFavourite()) return RoomType::Favorite; return RoomType::Invited;
if (room->isDirectChat()) return RoomType::Direct; if (room->isFavourite())
if (room->isLowPriority()) return RoomType::Deprioritized; return RoomType::Favorite;
if (room->isDirectChat())
return RoomType::Direct;
if (room->isLowPriority())
return RoomType::Deprioritized;
return RoomType::Normal; return RoomType::Normal;
} }
if (role == UnreadCountRole) return room->unreadCount(); if (role == UnreadCountRole)
if (role == NotificationCountRole) return room->notificationCount(); return room->unreadCount();
if (role == HighlightCountRole) return room->highlightCount(); if (role == NotificationCountRole)
if (role == LastEventRole) return room->lastEvent(); return room->notificationCount();
if (role == LastActiveTimeRole) return room->lastActiveTime(); if (role == HighlightCountRole)
if (role == CurrentRoomRole) return QVariant::fromValue(room); 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(); return QVariant();
} }
@ -200,6 +236,7 @@ QHash<int, QByteArray> RoomListModel::roleNames() const {
roles[HighlightCountRole] = "highlightCount"; roles[HighlightCountRole] = "highlightCount";
roles[LastEventRole] = "lastEvent"; roles[LastEventRole] = "lastEvent";
roles[LastActiveTimeRole] = "lastActiveTime"; roles[LastActiveTimeRole] = "lastActiveTime";
roles[JoinStateRole] = "joinState";
roles[CurrentRoomRole] = "currentRoom"; roles[CurrentRoomRole] = "currentRoom";
return roles; return roles;
} }

View File

@ -17,8 +17,8 @@ class RoomType : public QObject {
enum Types { enum Types {
Invited = 1, Invited = 1,
Favorite, Favorite,
Normal,
Direct, Direct,
Normal,
Deprioritized, Deprioritized,
}; };
REGISTER_ENUM(Types) REGISTER_ENUM(Types)
@ -39,6 +39,7 @@ class RoomListModel : public QAbstractListModel {
HighlightCountRole, HighlightCountRole,
LastEventRole, LastEventRole,
LastActiveTimeRole, LastActiveTimeRole,
JoinStateRole,
CurrentRoomRole, CurrentRoomRole,
}; };
@ -75,9 +76,12 @@ class RoomListModel : public QAbstractListModel {
signals: signals:
void connectionChanged(); void connectionChanged();
void roomAdded(SpectralRoom* room); void roomAdded(SpectralRoom* room);
void newMessage(const QString& roomId, const QString& eventId, void newMessage(const QString& roomId,
const QString& roomName, const QString& senderName, const QString& eventId,
const QString& text, const QImage& icon); const QString& roomName,
const QString& senderName,
const QString& text,
const QImage& icon);
}; };
#endif // ROOMLISTMODEL_H #endif // ROOMLISTMODEL_H

View File

@ -6,6 +6,7 @@
#include "csapi/content-repo.h" #include "csapi/content-repo.h"
#include "csapi/leaving.h" #include "csapi/leaving.h"
#include "csapi/typing.h" #include "csapi/typing.h"
#include "events/accountdataevents.h"
#include "events/typingevent.h" #include "events/typingevent.h"
#include <QFileDialog> #include <QFileDialog>
@ -18,7 +19,8 @@
#include "utils.h" #include "utils.h"
SpectralRoom::SpectralRoom(Connection* connection, QString roomId, SpectralRoom::SpectralRoom(Connection* connection,
QString roomId,
JoinState joinState) JoinState joinState)
: Room(connection, std::move(roomId), joinState) { : Room(connection, std::move(roomId), joinState) {
connect(this, &SpectralRoom::notificationCountChanged, this, connect(this, &SpectralRoom::notificationCountChanged, this,
@ -70,21 +72,21 @@ void SpectralRoom::chooseAndUploadFile() {
} }
} }
void SpectralRoom::saveFileAs(QString eventId) { void SpectralRoom::acceptInvitation() {
auto fileName = QFileDialog::getSaveFileName(Q_NULLPTR, tr("Save File as"), connection()->joinRoom(id());
fileNameToDownload(eventId));
if (!fileName.isEmpty()) downloadFile(eventId, QUrl::fromLocalFile(fileName));
} }
void SpectralRoom::acceptInvitation() { connection()->joinRoom(id()); } void SpectralRoom::forget() {
connection()->forgetRoom(id());
void SpectralRoom::forget() { connection()->forgetRoom(id()); } }
bool SpectralRoom::hasUsersTyping() { bool SpectralRoom::hasUsersTyping() {
QList<User*> users = usersTyping(); QList<User*> users = usersTyping();
if (users.isEmpty()) return false; if (users.isEmpty())
return false;
int count = users.length(); int count = users.length();
if (users.contains(localUser())) count--; if (users.contains(localUser()))
count--;
return count != 0; return count != 0;
} }
@ -104,10 +106,11 @@ void SpectralRoom::sendTypingNotification(bool isTyping) {
} }
QString SpectralRoom::lastEvent() { QString SpectralRoom::lastEvent() {
if (timelineSize() == 0) return ""; if (timelineSize() == 0)
return "";
const RoomEvent* lastEvent = messageEvents().rbegin()->get(); const RoomEvent* lastEvent = messageEvents().rbegin()->get();
return user(lastEvent->senderId())->displayname() + ": " + return user(lastEvent->senderId())->displayname() + ": " +
utils::removeReply(utils::eventToString(*lastEvent, this)); utils::removeReply(eventToString(*lastEvent));
} }
bool SpectralRoom::isEventHighlighted(const RoomEvent* e) const { bool SpectralRoom::isEventHighlighted(const RoomEvent* e) const {
@ -116,7 +119,8 @@ bool SpectralRoom::isEventHighlighted(const RoomEvent* e) const {
void SpectralRoom::checkForHighlights(const QMatrixClient::TimelineItem& ti) { void SpectralRoom::checkForHighlights(const QMatrixClient::TimelineItem& ti) {
auto localUserId = localUser()->id(); auto localUserId = localUser()->id();
if (ti->senderId() == localUserId) return; if (ti->senderId() == localUserId)
return;
if (auto* e = ti.viewAs<RoomMessageEvent>()) { if (auto* e = ti.viewAs<RoomMessageEvent>()) {
const auto& text = e->plainBody(); const auto& text = e->plainBody();
if (text.contains(localUserId) || if (text.contains(localUserId) ||
@ -142,8 +146,10 @@ void SpectralRoom::countChanged() {
} }
} }
void SpectralRoom::sendReply(QString userId, QString eventId, void SpectralRoom::sendReply(QString userId,
QString replyContent, QString sendContent) { QString eventId,
QString replyContent,
QString sendContent) {
QJsonObject json{ QJsonObject json{
{"msgtype", "m.text"}, {"msgtype", "m.text"},
{"body", "> <" + userId + "> " + replyContent + "\n\n" + sendContent}, {"body", "> <" + userId + "> " + replyContent + "\n\n" + sendContent},
@ -159,7 +165,8 @@ void SpectralRoom::sendReply(QString userId, QString eventId,
} }
QDateTime SpectralRoom::lastActiveTime() { QDateTime SpectralRoom::lastActiveTime() {
if (timelineSize() == 0) return QDateTime(); if (timelineSize() == 0)
return QDateTime();
return messageEvents().rbegin()->get()->timestamp(); return messageEvents().rbegin()->get()->timestamp();
} }
@ -205,15 +212,19 @@ QVariantList SpectralRoom::getUsers(const QString& prefix) {
} }
QString SpectralRoom::postMarkdownText(const QString& markdown) { QString SpectralRoom::postMarkdownText(const QString& markdown) {
unsigned char *sequence = (unsigned char *) qstrdup(markdown.toUtf8().constData()); unsigned char* sequence =
qint64 length = strlen((char *) sequence); (unsigned char*)qstrdup(markdown.toUtf8().constData());
qint64 length = strlen((char*)sequence);
hoedown_renderer* renderer = hoedown_html_renderer_new(HOEDOWN_HTML_USE_XHTML, 32); hoedown_renderer* renderer =
hoedown_extensions extensions = (hoedown_extensions) ((HOEDOWN_EXT_BLOCK | HOEDOWN_EXT_SPAN | HOEDOWN_EXT_MATH_EXPLICIT) & ~HOEDOWN_EXT_QUOTE); hoedown_html_renderer_new(HOEDOWN_HTML_USE_XHTML, 32);
hoedown_extensions extensions = (hoedown_extensions)(
(HOEDOWN_EXT_BLOCK | HOEDOWN_EXT_SPAN | HOEDOWN_EXT_MATH_EXPLICIT) &
~HOEDOWN_EXT_QUOTE);
hoedown_document* document = hoedown_document_new(renderer, extensions, 32); hoedown_document* document = hoedown_document_new(renderer, extensions, 32);
hoedown_buffer* html = hoedown_buffer_new(length); hoedown_buffer* html = hoedown_buffer_new(length);
hoedown_document_render(document, html, sequence, length); hoedown_document_render(document, html, sequence, length);
QString result = QString::fromUtf8((char *) html->data, html->size); QString result = QString::fromUtf8((char*)html->data, html->size);
free(sequence); free(sequence);
hoedown_buffer_free(html); hoedown_buffer_free(html);

View File

@ -8,6 +8,13 @@
#include <QPointer> #include <QPointer>
#include <QTimer> #include <QTimer>
#include <events/redactionevent.h>
#include <events/roomavatarevent.h>
#include <events/roomcreateevent.h>
#include <events/roommemberevent.h>
#include <events/roommessageevent.h>
#include <events/simplestateevents.h>
using namespace QMatrixClient; using namespace QMatrixClient;
class SpectralRoom : public Room { class SpectralRoom : public Room {
@ -23,7 +30,8 @@ class SpectralRoom : public Room {
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
public: public:
explicit SpectralRoom(Connection* connection, QString roomId, explicit SpectralRoom(Connection* connection,
QString roomId,
JoinState joinState = {}); JoinState joinState = {});
const QString& cachedInput() const { return m_cachedInput; } const QString& cachedInput() const { return m_cachedInput; }
@ -76,6 +84,148 @@ class SpectralRoom : public Room {
Q_INVOKABLE QString postMarkdownText(const QString& markdown); Q_INVOKABLE QString postMarkdownText(const QString& markdown);
template <typename BaseEventT>
QString eventToString(const BaseEventT& evt,
Qt::TextFormat format = Qt::PlainText) {
bool prettyPrint = (format == Qt::RichText);
using namespace QMatrixClient;
return visit(
evt,
[this, prettyPrint](const RoomMessageEvent& e) {
using namespace MessageEventContent;
if (prettyPrint && e.hasTextContent() &&
e.mimeType().name() != "text/plain")
return static_cast<const TextContent*>(e.content())->body;
if (e.hasFileContent()) {
auto fileCaption =
e.content()->fileInfo()->originalName.toHtmlEscaped();
if (fileCaption.isEmpty()) {
if (prettyPrint)
fileCaption = this->prettyPrint(e.plainBody());
else
fileCaption = e.plainBody();
}
return !fileCaption.isEmpty() ? fileCaption : tr("a file");
}
return prettyPrint ? this->prettyPrint(e.plainBody()) : 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);
FALLTHROUGH;
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 the display name");
else
text = tr("changed the display name to %1")
.arg(e.displayName().toHtmlEscaped());
}
if (e.isAvatarUpdate()) {
if (!text.isEmpty())
text += " and ";
if (e.avatarUrl().isEmpty())
text += tr("cleared the avatar");
else
text += tr("updated the 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());
},
[this, prettyPrint](const RoomTopicEvent& e) {
return (e.topic().isEmpty())
? tr("cleared the topic")
: tr("set the topic to: %1")
.arg(prettyPrint ? this->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"));
}
private: private:
QString m_cachedInput; QString m_cachedInput;
QSet<const QMatrixClient::RoomEvent*> highlights; QSet<const QMatrixClient::RoomEvent*> highlights;
@ -101,11 +251,12 @@ class SpectralRoom : public Room {
public slots: public slots:
void chooseAndUploadFile(); void chooseAndUploadFile();
void saveFileAs(QString eventId);
void acceptInvitation(); void acceptInvitation();
void forget(); void forget();
void sendTypingNotification(bool isTyping); void sendTypingNotification(bool isTyping);
void sendReply(QString userId, QString eventId, QString replyContent, void sendReply(QString userId,
QString eventId,
QString replyContent,
QString sendContent); QString sendContent);
}; };

View File

@ -14,14 +14,16 @@ UserListModel::UserListModel(QObject* parent)
: QAbstractListModel(parent), m_currentRoom(nullptr) {} : QAbstractListModel(parent), m_currentRoom(nullptr) {}
void UserListModel::setRoom(QMatrixClient::Room* room) { void UserListModel::setRoom(QMatrixClient::Room* room) {
if (m_currentRoom == room) return; if (m_currentRoom == room)
return;
using namespace QMatrixClient; using namespace QMatrixClient;
beginResetModel(); beginResetModel();
if (m_currentRoom) { if (m_currentRoom) {
m_currentRoom->disconnect(this); m_currentRoom->disconnect(this);
// m_currentRoom->connection()->disconnect(this); // m_currentRoom->connection()->disconnect(this);
for (User* user : m_users) user->disconnect(this); for (User* user : m_users)
user->disconnect(this);
m_users.clear(); m_users.clear();
} }
m_currentRoom = room; m_currentRoom = room;
@ -49,12 +51,14 @@ void UserListModel::setRoom(QMatrixClient::Room* room) {
} }
QMatrixClient::User* UserListModel::userAt(QModelIndex index) { QMatrixClient::User* UserListModel::userAt(QModelIndex index) {
if (index.row() < 0 || index.row() >= m_users.size()) return nullptr; if (index.row() < 0 || index.row() >= m_users.size())
return nullptr;
return m_users.at(index.row()); return m_users.at(index.row());
} }
QVariant UserListModel::data(const QModelIndex& index, int role) const { QVariant UserListModel::data(const QModelIndex& index, int role) const {
if (!index.isValid()) return QVariant(); if (!index.isValid())
return QVariant();
if (index.row() >= m_users.count()) { if (index.row() >= m_users.count()) {
qDebug() qDebug()
@ -71,12 +75,16 @@ QVariant UserListModel::data(const QModelIndex& index, int role) const {
if (role == AvatarRole) { if (role == AvatarRole) {
return user->avatarMediaId(); return user->avatarMediaId();
} }
if (role == ObjectRole) {
return QVariant::fromValue(user);
}
return QVariant(); return QVariant();
} }
int UserListModel::rowCount(const QModelIndex& parent) const { int UserListModel::rowCount(const QModelIndex& parent) const {
if (parent.isValid()) return 0; if (parent.isValid())
return 0;
return m_users.count(); return m_users.count();
} }
@ -111,7 +119,8 @@ void UserListModel::refresh(QMatrixClient::User* user, QVector<int> roles) {
void UserListModel::avatarChanged(QMatrixClient::User* user, void UserListModel::avatarChanged(QMatrixClient::User* user,
const QMatrixClient::Room* context) { const QMatrixClient::Room* context) {
if (context == m_currentRoom) refresh(user, {AvatarRole}); if (context == m_currentRoom)
refresh(user, {AvatarRole});
} }
int UserListModel::findUserPos(User* user) const { int UserListModel::findUserPos(User* user) const {
@ -127,5 +136,6 @@ QHash<int, QByteArray> UserListModel::roleNames() const {
roles[NameRole] = "name"; roles[NameRole] = "name";
roles[UserIDRole] = "userId"; roles[UserIDRole] = "userId";
roles[AvatarRole] = "avatar"; roles[AvatarRole] = "avatar";
roles[ObjectRole] = "user";
return roles; return roles;
} }

View File

@ -17,7 +17,12 @@ class UserListModel : public QAbstractListModel {
Q_PROPERTY( Q_PROPERTY(
QMatrixClient::Room* room READ room WRITE setRoom NOTIFY roomChanged) QMatrixClient::Room* room READ room WRITE setRoom NOTIFY roomChanged)
public: public:
enum EventRoles { NameRole = Qt::UserRole + 1, UserIDRole, AvatarRole }; enum EventRoles {
NameRole = Qt::UserRole + 1,
UserIDRole,
AvatarRole,
ObjectRole
};
using User = QMatrixClient::User; using User = QMatrixClient::User;

View File

@ -26,120 +26,6 @@ static const QRegularExpression userPillRegExp{
QString removeReply(const QString& text); QString removeReply(const QString& text);
QString cleanHTML(const QString& text, QMatrixClient::Room* room); QString cleanHTML(const QString& text, QMatrixClient::Room* room);
template <typename BaseEventT>
QString eventToString(const BaseEventT& evt,
QMatrixClient::Room* room = nullptr,
Qt::TextFormat format = Qt::PlainText) {
bool prettyPrint = (format == Qt::RichText);
using namespace QMatrixClient;
return visit(
evt,
[room, prettyPrint](const RoomMessageEvent& e) {
using namespace MessageEventContent;
if (prettyPrint && e.hasTextContent() &&
e.mimeType().name() != "text/plain") {
return cleanHTML(static_cast<const TextContent*>(e.content())->body,
room);
}
if (e.hasFileContent()) {
auto fileCaption = e.content()->fileInfo()->originalName;
if (fileCaption.isEmpty())
fileCaption = prettyPrint && room ? room->prettyPrint(e.plainBody())
: e.plainBody();
if (fileCaption.isEmpty()) return QObject::tr("a file");
}
return prettyPrint && room ? room->prettyPrint(e.plainBody())
: e.plainBody();
},
[room](const RoomMemberEvent& e) {
// FIXME: Rewind to the name that was at the time of this event
QString subjectName =
room ? room->roomMembername(e.userId()) : e.userId();
// The below code assumes senderName output in AuthorRole
switch (e.membership()) {
case MembershipType::Invite:
if (e.repeatsState())
return QObject::tr("reinvited %1 to the room").arg(subjectName);
FALLTHROUGH;
case MembershipType::Join: {
if (e.repeatsState())
return QObject::tr("joined the room (repeated)");
if (!e.prevContent() ||
e.membership() != e.prevContent()->membership) {
return e.membership() == MembershipType::Invite
? QObject::tr("invited %1 to the room")
.arg(subjectName)
: QObject::tr("joined the room");
}
QString text{};
if (e.isRename()) {
if (e.displayName().isEmpty())
text = QObject::tr("cleared their display name");
else
text = QObject::tr("changed their display name to %1")
.arg(e.displayName());
}
if (e.isAvatarUpdate()) {
if (!text.isEmpty()) text += " and ";
if (e.avatarUrl().isEmpty())
text += QObject::tr("cleared the avatar");
else
text += QObject::tr("updated the avatar");
}
return text;
}
case MembershipType::Leave:
if (e.prevContent() &&
e.prevContent()->membership == MembershipType::Ban) {
return (e.senderId() != e.userId())
? QObject::tr("unbanned %1").arg(subjectName)
: QObject::tr("self-unbanned");
}
return (e.senderId() != e.userId())
? QObject::tr("has kicked %1 from the room")
.arg(subjectName)
: QObject::tr("left the room");
case MembershipType::Ban:
return (e.senderId() != e.userId())
? QObject::tr("banned %1 from the room ")
.arg(subjectName)
: QObject::tr(" self-banned from the room ");
case MembershipType::Knock:
return QObject::tr("knocked");
default:;
}
return QObject::tr("made something unknown");
},
[](const RoomAliasesEvent& e) {
return QObject::tr("set aliases to: %1").arg(e.aliases().join(","));
},
[](const RoomCanonicalAliasEvent& e) {
return (e.alias().isEmpty())
? QObject::tr("cleared the room main alias")
: QObject::tr("set the room main alias to: %1")
.arg(e.alias());
},
[](const RoomNameEvent& e) {
return (e.name().isEmpty())
? QObject::tr("cleared the room name")
: QObject::tr("set the room name to: %1").arg(e.name());
},
[](const RoomTopicEvent& e) {
return (e.topic().isEmpty())
? QObject::tr("cleared the topic")
: QObject::tr("set the topic to: %1").arg(e.topic());
},
[](const RoomAvatarEvent&) {
return QObject::tr("changed the room avatar");
},
[](const EncryptionEvent&) {
return QObject::tr("activated End-to-End Encryption");
},
QObject::tr("Unknown Event"));
};
} // namespace utils } // namespace utils
#endif #endif