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:
DEPLOY_DIR: Spectral-%APPVEYOR_BUILD_VERSION%
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"
PLATFORM:

View File

@ -3,30 +3,42 @@ stages:
- deploy
build-flatpak:
image: black0/flatpak
image: registry.gitlab.com/b0/flatpak-kde-docker
stage: build
before_script:
- git submodule update --init --recursive
script:
- 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
- cd ../
cache:
key: "flatpak-$CI_COMMIT_REF_SLUG"
paths:
- flatpak/.flatpak-builder
artifacts:
paths:
- flatpak/spectral.flatpak
build-appimage:
image: black0/qt
image: registry.gitlab.com/b0/qt-docker
stage: build
before_script:
- git submodule update --init --recursive
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/qmake CONFIG+=debug CONFIG+=qml_debug PREFIX=/usr
- /opt/qt512/bin/qmake CONFIG+=debug CONFIG+=qml_debug CONFIG+=ccache PREFIX=/usr
- make
- 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
cache:
key: "appimage-$CI_COMMIT_REF_SLUG"
paths:
- ccache/
artifacts:
paths:
- 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.
It is at [Spectral Doc](https://doc.spectral.encom.eu.org/)
It is at [Spectral Doc](https://b0.gitlab.io/spectral-doc/)
## Contact
@ -31,6 +31,10 @@ This program uses libqmatrixclient library and some C++ models from Quaternion.
[libqmatrixclient](https://github.com/QMatrixClient/libqmatrixclient)
This program includes the source code of hoedown.
[Hoedown](https://github.com/hoedown/hoedown)
## Donation
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.
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
- --filesystem=xdg-download
- --talk-name=org.freedesktop.Notifications
- --talk-name=org.kde.StatusNotifierWatcher
modules:
- name: spectral
buildsystem: qmake
config-opts:
- "BUNDLE_FONT=true"
sources:
- type: dir
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 secondaryClicked()
acceptedButtons: MSettings.pressAndHold ? Qt.LeftButton : (Qt.LeftButton | Qt.RightButton)
acceptedButtons: Qt.LeftButton | Qt.RightButton
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.Controls 2.12
import QtQuick.Controls.Material 2.3
TextField {
id: textField
selectByMouse: true
topPadding: 8
bottomPadding: 8
background: Item {
Label {
id: floatingPlaceholder
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: textField.topPadding
anchors.leftMargin: textField.leftPadding
transformOrigin: Item.TopLeft
visible: false
color: Material.accent
states: [
State {
name: "shown"
when: textField.text.length !== 0
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
radius: height / 2
color: stringToColor(hint)
antialiasing: true
Label {
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.Component 2.0
import Spectral.Dialog 2.0
import Spectral.Menu.Timeline 2.0
import Spectral.Font 0.1
import Spectral.Effect 2.0
ColumnLayout {
readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote" || aboveEventType === "other")
@ -52,6 +55,20 @@ ColumnLayout {
visible: avatarVisible
hint: author.displayName
source: author.avatarMediaId
Component {
id: userDetailDialog
UserDetailDialog {}
}
RippleEffect {
anchors.fill: parent
circular: true
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author}).open()
}
}
Label {
@ -94,7 +111,7 @@ ColumnLayout {
}
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
}
}
@ -109,52 +126,59 @@ ColumnLayout {
id: messageMouseArea
onSecondaryClicked: messageContextMenu.popup()
Menu {
id: messageContextMenu
MenuItem {
text: "View Source"
onTriggered: {
sourceDialog.sourceText = toolTip
sourceDialog.open()
}
}
MenuItem {
text: "Open Externally"
onTriggered: downloadAndOpen()
}
MenuItem {
text: "Save As"
onTriggered: saveFileAs()
}
MenuItem {
text: "Reply"
onTriggered: {
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()
}
}
MenuItem {
text: "Redact"
onTriggered: currentRoom.redactEvent(eventId)
Component {
id: messageSourceDialog
MessageSourceDialog {}
}
Component {
id: openFileDialog
OpenFileDialog {}
}
Component {
id: fileDelegateContextMenu
FileDelegateContextMenu {}
}
}
}
}
}
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()
{
@ -162,7 +186,7 @@ ColumnLayout {
else
{
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.Component 2.0
import Spectral.Dialog 2.0
import Spectral.Menu.Timeline 2.0
import Spectral.Effect 2.0
import Spectral.Font 0.1
ColumnLayout {
@ -16,13 +19,17 @@ ColumnLayout {
readonly property bool sentByMe: author === currentRoom.localUser
property bool openOnFinished: false
property bool showOnFinished: false
readonly property bool downloaded: progressInfo && progressInfo.completed
id: root
spacing: 0
onDownloadedChanged: if (downloaded && openOnFinished) openSavedFile()
onDownloadedChanged: {
if (downloaded && showOnFinished) showSavedFile()
if (downloaded && openOnFinished) openSavedFile()
}
Label {
Layout.leftMargin: 48
@ -52,6 +59,20 @@ ColumnLayout {
visible: avatarVisible
hint: author.displayName
source: author.avatarMediaId
Component {
id: userDetailDialog
UserDetailDialog {}
}
RippleEffect {
anchors.fill: parent
circular: true
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author}).open()
}
}
Label {
@ -69,16 +90,26 @@ ColumnLayout {
verticalAlignment: Label.AlignVCenter
}
BusyIndicator {
Layout.preferredWidth: 64
Layout.preferredHeight: 64
visible: img.status == Image.Loading
}
Image {
Layout.maximumWidth: messageListView.width - (!sentByMe ? 32 + messageRow.spacing : 0) - 48
id: img
source: downloaded ? progressInfo.localPath : "image://mxc/" +
source: "image://mxc/" +
(content.info && content.info.thumbnail_info ?
content.thumbnailMediaId : content.mediaId)
sourceSize.width: 200
sourceSize.height: 200
sourceSize.width: 720
sourceSize.height: 720
fillMode: Image.PreserveAspectCrop
layer.enabled: true
layer.effect: OpacityMask {
@ -94,61 +125,110 @@ ColumnLayout {
color: "transparent"
radius: 24
antialiasing: true
border.width: 2
border.width: 4
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
id: messageMouseArea
onSecondaryClicked: messageContextMenu.popup()
onPrimaryClicked: downloadAndShow()
Menu {
id: messageContextMenu
MenuItem {
text: "View Source"
onTriggered: {
sourceDialog.sourceText = toolTip
sourceDialog.open()
}
}
MenuItem {
text: "Open Externally"
onTriggered: downloadAndOpen()
}
MenuItem {
text: "Save As"
onTriggered: saveFileAs()
}
MenuItem {
text: "Reply"
onTriggered: {
onSecondaryClicked: {
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 {
text: "Redact"
onTriggered: currentRoom.redactEvent(eventId)
Component {
id: messageSourceDialog
MessageSourceDialog {}
}
Component {
id: openFileDialog
OpenFileDialog {}
}
Component {
id: imageDelegateContextMenu
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()
{
@ -156,7 +236,7 @@ ColumnLayout {
else
{
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.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 {
readonly property bool avatarVisible: !sentByMe && (aboveAuthor !== author || aboveSection !== section || aboveEventType === "state" || aboveEventType === "emote" || aboveEventType === "other")
@ -48,6 +50,20 @@ ColumnLayout {
visible: avatarVisible
hint: author.displayName
source: author.avatarMediaId
Component {
id: userDetailDialog
UserDetailDialog {}
}
RippleEffect {
anchors.fill: parent
circular: true
onClicked: userDetailDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom, "user": author}).open()
}
}
Label {
@ -74,43 +90,42 @@ ColumnLayout {
background: Rectangle {
color: sentByMe ? "#009DC2" : eventType === "notice" ? "#4285F4" : "#673AB7"
radius: 18
antialiasing: true
AutoMouseArea {
anchors.fill: parent
id: messageMouseArea
onSecondaryClicked: messageContextMenu.popup()
Menu {
readonly property string selectedText: contentLabel.selectedText
id: messageContextMenu
MenuItem {
text: "View Source"
onTriggered: {
sourceDialog.sourceText = toolTip
sourceDialog.open()
}
}
MenuItem {
text: "Reply"
onTriggered: {
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 = messageContextMenu.selectedText || message
roomPanelInput.replyContent = contentLabel.selectedText || message
roomPanelInput.isReply = true
roomPanelInput.focus()
})
contextMenu.redact.connect(function() {
currentRoom.redactEvent(eventId)
})
contextMenu.popup()
}
}
MenuItem {
text: "Redact"
onTriggered: currentRoom.redactEvent(eventId)
Component {
id: messageDelegateContextMenu
MessageDelegateContextMenu {}
}
Component {
id: messageSourceDialog
MessageSourceDialog {}
}
}
}
@ -123,19 +138,10 @@ ColumnLayout {
padding: 8
background: Item {
Rectangle {
anchors.leftMargin: 0
width: 2
height: parent.height
color: "white"
}
MouseArea {
background: RippleEffect {
anchors.fill: parent
onClicked: goToEvent(replyEventId)
}
onPrimaryClicked: goToEvent(replyEventId)
}
contentItem: RowLayout {
@ -169,7 +175,7 @@ ColumnLayout {
Layout.fillWidth: true
color: "white"
text: replyDisplay || ""
text: "<style>a{color: white;} .user-pill{}</style>" + (replyDisplay || "")
wrapMode: Label.Wrap
textFormat: Label.RichText
@ -178,6 +184,14 @@ ColumnLayout {
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
visible: replyEventId || ""
color: "white"
}
TextEdit {
Layout.fillWidth: true
@ -187,7 +201,7 @@ ColumnLayout {
color: "white"
font.family: CommonFont.font.family
font.family: window.font.family
font.pixelSize: 14
selectByMouse: true
readOnly: true

View File

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

View File

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

View File

@ -5,6 +5,8 @@ import QtQuick.Layouts 1.12
import QtQuick.Controls.Material 2.12
import Spectral.Component 2.0
import Spectral.Dialog 2.0
import Spectral.Menu 2.0
import Spectral.Effect 2.0
import Spectral 0.1
@ -13,12 +15,11 @@ import Spectral.Setting 0.1
import SortFilterProxyModel 0.2
Item {
property var controller: null
readonly property var user: controller.connection ? controller.connection.localUser : null
property var connection: null
readonly property var user: connection ? connection.localUser : null
property int filter: 0
property var enteredRoom: null
property alias errorControl: errorControl
signal enterRoom(var room)
signal leaveRoom(var room)
@ -28,7 +29,7 @@ Item {
RoomListModel {
id: roomListModel
connection: controller.connection
connection: root.connection
onNewMessage: if (!window.active && MSettings.showNotification) spectralController.postNotification(roomId, eventId, roomName, senderName, text, icon)
}
@ -44,8 +45,8 @@ Item {
switch (category) {
case 1: return "Invited"
case 2: return "Favorites"
case 3: return "Rooms"
case 4: return "People"
case 3: return "People"
case 4: return "Rooms"
case 5: return "Low Priority"
}
}
@ -60,6 +61,9 @@ Item {
]
filters: [
ExpressionFilter {
expression: joinState != "upgraded"
},
RegExpFilter {
roleName: "name"
pattern: searchField.text
@ -71,483 +75,15 @@ Item {
},
ExpressionFilter {
enabled: filter === 2
expression: category === 1 || category === 2 || category === 4
expression: category === 1 || category === 2 || category === 3
},
ExpressionFilter {
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 {
anchors.fill: parent
spacing: 0
@ -564,7 +100,7 @@ Item {
rightPadding: 18
contentItem: RowLayout {
ItemDelegate {
ToolButton {
Layout.preferredWidth: height
Layout.fillHeight: true
@ -614,7 +150,7 @@ Item {
onClicked: filterMenu.popup()
}
ItemDelegate {
ToolButton {
Layout.preferredWidth: height
Layout.fillHeight: true
@ -627,40 +163,14 @@ Item {
AutoTextField {
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.fillHeight: true
Layout.alignment: Qt.AlignVCenter
id: searchField
topPadding: 0
bottomPadding: 0
placeholderText: "Search..."
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 {
@ -673,9 +183,12 @@ Item {
source: root.user ? root.user.avatarMediaId : null
hint: root.user ? root.user.displayName : "?"
MouseArea {
RippleEffect {
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 {
Layout.fillWidth: true
Layout.fillHeight: true
@ -766,7 +233,7 @@ Item {
}
Rectangle {
width: unreadCount > 0 ? 4 : 0
width: unreadCount >= 0 ? 4 : 0
height: parent.height
color: Material.accent
@ -854,11 +321,9 @@ Item {
RippleEffect {
anchors.fill: parent
onSecondaryClicked: roomContextMenu.popup()
onPrimaryClicked: {
if (category === RoomType.Invited) {
inviteDialog.currentRoom = currentRoom
inviteDialog.open()
acceptInvitationDialog.createObject(ApplicationWindow.overlay, {"room": currentRoom}).open()
} else {
if (enteredRoom) {
enteredRoom.displayed = false
@ -869,36 +334,13 @@ Item {
enteredRoom = currentRoom
}
}
onSecondaryClicked: roomListContextMenu.createObject(ApplicationWindow.overlay, {"room": currentRoom}).popup()
}
Menu {
id: roomContextMenu
Component {
id: roomListContextMenu
MenuItem {
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()
}
RoomListContextMenu {}
}
}
@ -917,42 +359,9 @@ Item {
}
}
Dialog {
property var currentRoom
Component {
id: acceptInvitationDialog
id: inviteDialog
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()
}
}
AcceptInvitationDialog {}
}
}

View File

@ -23,27 +23,42 @@ Item {
room: currentRoom
}
RoomDrawer {
width: Math.min(root.width * 0.7, 480)
height: root.height
Column {
anchors.centerIn: parent
id: roomDrawer
spacing: 16
room: currentRoom
visible: !currentRoom
Image {
anchors.horizontalCenter: parent.horizontalCenter
width: 240
fillMode: Image.PreserveAspectFit
source: "qrc:/assets/img/matrix.svg"
}
Label {
anchors.centerIn: parent
visible: !currentRoom
text: "Please choose a room."
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 {
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
}
@ -64,7 +79,7 @@ Item {
topic: currentRoom ? (currentRoom.topic).replace(/(\r\n\t|\n|\r\t)/gm,"") : ""
atTop: messageListView.atYBeginning
onClicked: roomDrawer.open()
onClicked: roomDrawer.visible ? roomDrawer.close() : roomDrawer.open()
}
ColumnLayout {
@ -92,15 +107,16 @@ Item {
highlightMoveDuration: 500
boundsBehavior: Flickable.DragOverBounds
model: SortFilterProxyModel {
id: sortedMessageEventModel
sourceModel: messageEventModel
filters: ExpressionFilter {
filters: [
ExpressionFilter {
expression: marks !== 0x10 && eventType !== "other"
}
]
onModelReset: {
if (currentRoom) {
@ -182,8 +198,7 @@ Item {
visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000
}
MessageDelegate {
}
MessageDelegate {}
}
}
@ -200,8 +215,7 @@ Item {
visible: section !== aboveSection || Math.abs(time - aboveTime) > 600000
}
MessageDelegate {
}
MessageDelegate {}
}
}
@ -244,24 +258,21 @@ Item {
}
}
RoundButton {
width: 64
height: 64
anchors.right: parent.right
Button {
anchors.top: parent.top
id: goBottomFab
anchors.horizontalCenter: parent.horizontalCenter
visible: currentRoom && currentRoom.hasUnreadMessages
contentItem: MaterialIcon {
anchors.fill: parent
topPadding: 8
bottomPadding: 8
leftPadding: 24
rightPadding: 24
icon: "\ue316"
color: "white"
}
Material.foreground: MPalette.foreground
Material.background: MPalette.banner
Material.background: Material.accent
text: "Go to read marker"
onClicked: goToEvent(currentRoom.readMarkerEventId)
}
@ -287,85 +298,6 @@ Item {
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 {

View File

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

View File

@ -5,11 +5,11 @@ import Qt.labs.settings 1.0
Settings {
property bool showNotification: true
property bool pressAndHold
property bool showTray: true
property bool darkTheme
property bool enableTimelineBackground: true
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.Component 2.0
import Spectral.Page 2.0
import Spectral.Dialog 2.0
import Spectral.Effect 2.0
import Spectral 0.1
import Spectral.Setting 0.1
ApplicationWindow {
readonly property bool inPortrait: window.width < window.height
Material.theme: MPalette.theme
Material.background: MPalette.background
width: 960
height: 640
minimumWidth: 720
minimumWidth: 480
minimumHeight: 360
id: window
@ -27,6 +29,8 @@ ApplicationWindow {
visible: true
title: qsTr("Spectral")
font.family: MSettings.fontFamily
background: Rectangle {
color: MSettings.darkTheme ? "#303030" : "#FFFFFF"
}
@ -37,16 +41,14 @@ ApplicationWindow {
menu: Platform.Menu {
Platform.MenuItem {
text: qsTr("Hide Window")
onTriggered: hideWindow()
text: qsTr("Toggle Window")
onTriggered: window.visible ? hideWindow() : showWindow()
}
Platform.MenuItem {
text: qsTr("Quit")
onTriggered: Qt.quit()
}
}
onActivated: showWindow()
}
Controller {
@ -59,130 +61,86 @@ ApplicationWindow {
roomForm.goToEvent(eventId)
showWindow()
}
onErrorOccured: {
roomListForm.errorControl.error = error
roomListForm.errorControl.detail = detail
roomListForm.errorControl.visible = true
}
onSyncDone: roomListForm.errorControl.visible = false
onErrorOccured: errorControl.show(error + ": " + detail, 3000)
}
Shortcut {
sequence: StandardKey.Quit
sequence: "Ctrl+Q"
context: Qt.ApplicationShortcut
onActivated: Qt.quit()
}
Dialog {
property bool busy: false
width: 360
x: (window.width - width) / 2
y: (window.height - height) / 2
id: loginDialog
ToolTip {
id: errorControl
parent: ApplicationWindow.overlay
title: "Login"
contentItem: Column {
AutoTextField {
width: parent.width
id: serverField
placeholderText: "Server Address"
text: "https://matrix.org"
font.pixelSize: 14
}
AutoTextField {
width: parent.width
Component {
id: accountDetailDialog
id: usernameField
placeholderText: "Username"
AccountDetailDialog {}
}
AutoTextField {
width: parent.width
Component {
id: loginDialog
id: passwordField
placeholderText: "Password"
echoMode: TextInput.Password
}
LoginDialog {}
}
footer: DialogButtonBox {
Button {
text: "OK"
flat: true
enabled: !loginDialog.busy
Component {
id: joinRoomDialog
onClicked: loginDialog.doLogin()
JoinRoomDialog {}
}
Button {
text: "Cancel"
flat: true
enabled: !loginDialog.busy
Component {
id: createRoomDialog
onClicked: loginDialog.close()
CreateRoomDialog {}
}
ToolTip {
id: loginButtonTooltip
Component {
id: fontFamilyDialog
}
FontFamilyDialog {}
}
onVisibleChanged: {
if (visible) spectralController.onErrorOccured.connect(showError)
else spectralController.onErrorOccured.disconnect(showError)
Component {
id: chatBackgroundDialog
OpenFileDialog {}
}
function showError(error, detail) {
loginDialog.busy = false
loginButtonTooltip.text = error + ": " + detail
loginButtonTooltip.open()
}
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
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 {
anchors.fill: parent
id: roomListDrawer
RoomListPanel {
width: window.width * 0.35
Layout.minimumWidth: 180
anchors.fill: parent
id: roomListForm
clip: true
controller: spectralController
connection: spectralController.connection
onLeaveRoom: roomForm.saveReadMarker(room)
}
}
RoomPanel {
Layout.fillWidth: true
Layout.minimumWidth: 480
anchors.fill: parent
anchors.leftMargin: !inPortrait ? roomListDrawer.width : undefined
anchors.rightMargin: !inPortrait && roomDrawer.visible ? roomDrawer.width : undefined
id: roomForm
@ -190,6 +148,18 @@ ApplicationWindow {
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 {
@ -210,7 +180,7 @@ ApplicationWindow {
Component.onCompleted: {
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
Primary=#344955
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/Effect/ElevationEffect.qml</file>
<file>imports/Spectral/Effect/qmldir</file>
<file>imports/Spectral/Page/qmldir</file>
<file>assets/font/material.ttf</file>
<file>assets/img/icon.icns</file>
<file>assets/img/icon.ico</file>
@ -31,17 +30,31 @@
<file>imports/Spectral/Component/AutoListView.qml</file>
<file>imports/Spectral/Component/AutoTextField.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>assets/img/roompanel.svg</file>
<file>assets/img/matrix.svg</file>
<file>imports/Spectral/Effect/RippleEffect.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/Avatar.qml</file>
<file>imports/Spectral/Setting/Palette.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>
</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) {
USE_SYSTEM_QMATRIXCLIENT = false
}
isEmpty(BUNDLE_FONT) {
BUNDLE_FONT = false
}
$$USE_SYSTEM_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
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
QML_IMPORT_PATH += imports/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,13 @@
#include <QPointer>
#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;
class SpectralRoom : public Room {
@ -23,7 +30,8 @@ class SpectralRoom : public Room {
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
public:
explicit SpectralRoom(Connection* connection, QString roomId,
explicit SpectralRoom(Connection* connection,
QString roomId,
JoinState joinState = {});
const QString& cachedInput() const { return m_cachedInput; }
@ -76,6 +84,148 @@ class SpectralRoom : public Room {
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:
QString m_cachedInput;
QSet<const QMatrixClient::RoomEvent*> highlights;
@ -101,11 +251,12 @@ class SpectralRoom : public Room {
public slots:
void chooseAndUploadFile();
void saveFileAs(QString eventId);
void acceptInvitation();
void forget();
void sendTypingNotification(bool isTyping);
void sendReply(QString userId, QString eventId, QString replyContent,
void sendReply(QString userId,
QString eventId,
QString replyContent,
QString sendContent);
};

View File

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

View File

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

View File

@ -26,120 +26,6 @@ static const QRegularExpression userPillRegExp{
QString removeReply(const QString& text);
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
#endif